diff --git a/ee/clickhouse/models/test/test_cohort.py b/ee/clickhouse/models/test/test_cohort.py index 51ff0f2e8816f..00140e21f690d 100644 --- a/ee/clickhouse/models/test/test_cohort.py +++ b/ee/clickhouse/models/test/test_cohort.py @@ -16,6 +16,7 @@ from posthog.models.property.util import parse_prop_grouped_clauses from posthog.models.team import Team from posthog.queries.util import PersonPropertiesMode +from posthog.schema import PersonsOnEventsMode from posthog.test.base import ( BaseTest, ClickhouseTestMixin, @@ -25,7 +26,6 @@ snapshot_clickhouse_insert_cohortpeople_queries, snapshot_clickhouse_queries, ) -from posthog.utils import PersonOnEventsMode def _create_action(**kwargs): @@ -145,7 +145,7 @@ def test_prop_cohort_basic_action(self): team_id=self.team.pk, property_group=filter.property_groups, person_properties_mode=PersonPropertiesMode.USING_SUBQUERY - if self.team.person_on_events_mode == PersonOnEventsMode.DISABLED + if self.team.person_on_events_mode == PersonsOnEventsMode.disabled else PersonPropertiesMode.DIRECT_ON_EVENTS, hogql_context=filter.hogql_context, ) @@ -200,7 +200,7 @@ def test_prop_cohort_basic_event_days(self): team_id=self.team.pk, property_group=filter.property_groups, person_properties_mode=PersonPropertiesMode.USING_SUBQUERY - if self.team.person_on_events_mode == PersonOnEventsMode.DISABLED + if self.team.person_on_events_mode == PersonsOnEventsMode.disabled else PersonPropertiesMode.DIRECT_ON_EVENTS, hogql_context=filter.hogql_context, ) @@ -225,7 +225,7 @@ def test_prop_cohort_basic_event_days(self): team_id=self.team.pk, property_group=filter.property_groups, person_properties_mode=PersonPropertiesMode.USING_SUBQUERY - if self.team.person_on_events_mode == PersonOnEventsMode.DISABLED + if self.team.person_on_events_mode == PersonsOnEventsMode.disabled else PersonPropertiesMode.DIRECT_ON_EVENTS, hogql_context=filter.hogql_context, ) @@ -276,7 +276,7 @@ def test_prop_cohort_basic_action_days(self): team_id=self.team.pk, property_group=filter.property_groups, person_properties_mode=PersonPropertiesMode.USING_SUBQUERY - if self.team.person_on_events_mode == PersonOnEventsMode.DISABLED + if self.team.person_on_events_mode == PersonsOnEventsMode.disabled else PersonPropertiesMode.DIRECT_ON_EVENTS, hogql_context=filter.hogql_context, ) @@ -297,7 +297,7 @@ def test_prop_cohort_basic_action_days(self): team_id=self.team.pk, property_group=filter.property_groups, person_properties_mode=PersonPropertiesMode.USING_SUBQUERY - if self.team.person_on_events_mode == PersonOnEventsMode.DISABLED + if self.team.person_on_events_mode == PersonsOnEventsMode.disabled else PersonPropertiesMode.DIRECT_ON_EVENTS, hogql_context=filter.hogql_context, ) diff --git a/ee/clickhouse/queries/enterprise_cohort_query.py b/ee/clickhouse/queries/enterprise_cohort_query.py index a007b54903bdd..a748a64adf06a 100644 --- a/ee/clickhouse/queries/enterprise_cohort_query.py +++ b/ee/clickhouse/queries/enterprise_cohort_query.py @@ -12,7 +12,7 @@ validate_seq_date_more_recent_than_date, ) from posthog.queries.util import PersonPropertiesMode -from posthog.utils import PersonOnEventsMode +from posthog.schema import PersonsOnEventsMode def check_negation_clause(prop: PropertyGroup) -> Tuple[bool, bool]: @@ -319,7 +319,7 @@ def _get_sequence_query(self) -> Tuple[str, Dict[str, Any], str]: event_param_name = f"{self._cohort_pk}_event_ids" - if self.should_pushdown_persons and self._person_on_events_mode != PersonOnEventsMode.DISABLED: + if self.should_pushdown_persons and self._person_on_events_mode != PersonsOnEventsMode.disabled: person_prop_query, person_prop_params = self._get_prop_groups( self._inner_property_groups, person_properties_mode=PersonPropertiesMode.DIRECT_ON_EVENTS, diff --git a/ee/clickhouse/queries/event_query.py b/ee/clickhouse/queries/event_query.py index 3e57be3e3892f..259b4c4894786 100644 --- a/ee/clickhouse/queries/event_query.py +++ b/ee/clickhouse/queries/event_query.py @@ -12,7 +12,7 @@ from posthog.models.property import PropertyName from posthog.models.team import Team from posthog.queries.event_query.event_query import EventQuery -from posthog.utils import PersonOnEventsMode +from posthog.schema import PersonsOnEventsMode class EnterpriseEventQuery(EventQuery): @@ -37,7 +37,7 @@ def __init__( extra_event_properties: List[PropertyName] = [], extra_person_fields: List[ColumnName] = [], override_aggregate_users_by_distinct_id: Optional[bool] = None, - person_on_events_mode: PersonOnEventsMode = PersonOnEventsMode.DISABLED, + person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.disabled, **kwargs, ) -> None: super().__init__( diff --git a/ee/clickhouse/queries/funnels/funnel_correlation.py b/ee/clickhouse/queries/funnels/funnel_correlation.py index ebe78d3b86826..3ca6801ee6af6 100644 --- a/ee/clickhouse/queries/funnels/funnel_correlation.py +++ b/ee/clickhouse/queries/funnels/funnel_correlation.py @@ -34,7 +34,8 @@ from posthog.queries.person_distinct_id_query import get_team_distinct_ids_query from posthog.queries.person_query import PersonQuery from posthog.queries.util import correct_result_for_sampling -from posthog.utils import PersonOnEventsMode, generate_short_id +from posthog.schema import PersonsOnEventsMode +from posthog.utils import generate_short_id class EventDefinition(TypedDict): @@ -155,7 +156,7 @@ def __init__( def properties_to_include(self) -> List[str]: props_to_include = [] if ( - self._team.person_on_events_mode != PersonOnEventsMode.DISABLED + self._team.person_on_events_mode != PersonsOnEventsMode.disabled and self._filter.correlation_type == FunnelCorrelationType.PROPERTIES ): # When dealing with properties, make sure funnel response comes with properties @@ -435,7 +436,7 @@ def get_properties_query(self) -> Tuple[str, Dict[str, Any]]: return query, params def _get_aggregation_target_join_query(self) -> str: - if self._team.person_on_events_mode == PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: + if self._team.person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: aggregation_person_join = f""" JOIN funnel_actors as actors ON event.person_id = actors.actor_id @@ -502,7 +503,7 @@ def _get_events_join_query(self) -> str: def _get_aggregation_join_query(self): if self._filter.aggregation_group_type_index is None: - if self._team.person_on_events_mode != PersonOnEventsMode.DISABLED and groups_on_events_querying_enabled(): + if self._team.person_on_events_mode != PersonsOnEventsMode.disabled and groups_on_events_querying_enabled(): return "", {} person_query, person_query_params = PersonQuery( @@ -522,7 +523,7 @@ def _get_aggregation_join_query(self): return GroupsJoinQuery(self._filter, self._team.pk, join_key="funnel_actors.actor_id").get_join_query() def _get_properties_prop_clause(self): - if self._team.person_on_events_mode != PersonOnEventsMode.DISABLED and groups_on_events_querying_enabled(): + if self._team.person_on_events_mode != PersonsOnEventsMode.disabled and groups_on_events_querying_enabled(): group_properties_field = f"group{self._filter.aggregation_group_type_index}_properties" aggregation_properties_alias = ( "person_properties" if self._filter.aggregation_group_type_index is None else group_properties_field @@ -549,7 +550,7 @@ def _get_properties_prop_clause(self): param_name = f"property_name_{index}" if self._filter.aggregation_group_type_index is not None: expression, _ = get_property_string_expr( - "groups" if self._team.person_on_events_mode == PersonOnEventsMode.DISABLED else "events", + "groups" if self._team.person_on_events_mode == PersonsOnEventsMode.disabled else "events", property_name, f"%({param_name})s", aggregation_properties_alias, @@ -557,12 +558,12 @@ def _get_properties_prop_clause(self): ) else: expression, _ = get_property_string_expr( - "person" if self._team.person_on_events_mode == PersonOnEventsMode.DISABLED else "events", + "person" if self._team.person_on_events_mode == PersonsOnEventsMode.disabled else "events", property_name, f"%({param_name})s", aggregation_properties_alias, materialised_table_column=aggregation_properties_alias - if self._team.person_on_events_mode != PersonOnEventsMode.DISABLED + if self._team.person_on_events_mode != PersonsOnEventsMode.disabled else "properties", ) person_property_params[param_name] = property_name diff --git a/ee/clickhouse/queries/groups_join_query.py b/ee/clickhouse/queries/groups_join_query.py index 886510dfc3541..db1d12a3c6c46 100644 --- a/ee/clickhouse/queries/groups_join_query.py +++ b/ee/clickhouse/queries/groups_join_query.py @@ -9,7 +9,7 @@ from posthog.models.property.util import parse_prop_grouped_clauses from posthog.models.team.team import groups_on_events_querying_enabled from posthog.queries.util import PersonPropertiesMode -from posthog.utils import PersonOnEventsMode +from posthog.schema import PersonsOnEventsMode class GroupsJoinQuery: @@ -27,7 +27,7 @@ def __init__( team_id: int, column_optimizer: Optional[EnterpriseColumnOptimizer] = None, join_key: Optional[str] = None, - person_on_events_mode: PersonOnEventsMode = PersonOnEventsMode.DISABLED, + person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.disabled, ) -> None: self._filter = filter self._team_id = team_id @@ -38,7 +38,7 @@ def __init__( def get_join_query(self) -> Tuple[str, Dict]: join_queries, params = [], {} - if self._person_on_events_mode != PersonOnEventsMode.DISABLED and groups_on_events_querying_enabled(): + if self._person_on_events_mode != PersonsOnEventsMode.disabled and groups_on_events_querying_enabled(): return "", {} for group_type_index in self._column_optimizer.group_types_to_query: diff --git a/ee/session_recordings/queries/test/test_session_recording_list_from_session_replay.py b/ee/session_recordings/queries/test/test_session_recording_list_from_session_replay.py index 33fdd6c3e0236..71196ec0ecadc 100644 --- a/ee/session_recordings/queries/test/test_session_recording_list_from_session_replay.py +++ b/ee/session_recordings/queries/test/test_session_recording_list_from_session_replay.py @@ -12,6 +12,7 @@ from posthog.clickhouse.client import sync_execute from posthog.models import Person from posthog.models.filters import SessionRecordingsFilter +from posthog.schema import PersonsOnEventsMode from posthog.session_recordings.queries.session_recording_list_from_replay_summary import ( SessionRecordingListFromReplaySummary, ) @@ -24,7 +25,6 @@ snapshot_clickhouse_queries, _create_event, ) -from posthog.utils import PersonOnEventsMode @freeze_time("2021-01-01T13:46:23") @@ -63,7 +63,7 @@ def create_event( True, False, False, - PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS, + PersonsOnEventsMode.person_id_no_override_properties_on_events, { "kperson_filter_pre__0": "rgInternal", "kpersonquery_person_filter_fin__0": "rgInternal", @@ -79,7 +79,7 @@ def create_event( False, False, False, - PersonOnEventsMode.DISABLED, + PersonsOnEventsMode.disabled, { "kperson_filter_pre__0": "rgInternal", "kpersonquery_person_filter_fin__0": "rgInternal", @@ -95,7 +95,7 @@ def create_event( False, True, False, - PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS, + PersonsOnEventsMode.person_id_override_properties_on_events, { "event_names": [], "event_start_time": mock.ANY, @@ -111,7 +111,7 @@ def create_event( False, True, True, - PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS, + PersonsOnEventsMode.person_id_override_properties_on_events, { "event_end_time": mock.ANY, "event_names": [], @@ -130,7 +130,7 @@ def test_effect_of_poe_settings_on_query_generated( poe_v1: bool, poe_v2: bool, allow_denormalized_props: bool, - expected_poe_mode: PersonOnEventsMode, + expected_poe_mode: PersonsOnEventsMode, expected_query_params: Dict, unmaterialized_person_column_used: bool, materialized_event_column_used: bool, diff --git a/ee/tasks/subscriptions/__init__.py b/ee/tasks/subscriptions/__init__.py index c4a87f515abc2..7ca0e06e6d529 100644 --- a/ee/tasks/subscriptions/__init__.py +++ b/ee/tasks/subscriptions/__init__.py @@ -117,8 +117,8 @@ def _deliver_subscription_report( raise NotImplementedError(f"{subscription.target_type} is not supported") if not is_new_subscription_target: - subscription.set_next_delivery_date() - subscription.save() + subscription.set_next_delivery_date(subscription.next_delivery_date) + subscription.save(update_fields=["next_delivery_date"]) @shared_task(queue=CeleryQueue.SUBSCRIPTION_DELIVERY.value) diff --git a/ee/tasks/test/subscriptions/test_slack_subscriptions.py b/ee/tasks/test/subscriptions/test_slack_subscriptions.py index 9770127e73778..758f74cd3b2d1 100644 --- a/ee/tasks/test/subscriptions/test_slack_subscriptions.py +++ b/ee/tasks/test/subscriptions/test_slack_subscriptions.py @@ -13,7 +13,7 @@ @patch("ee.tasks.subscriptions.slack_subscriptions.SlackIntegration") -@freeze_time("2022-02-02T08:55:00.000Z") +@freeze_time("2022-02-02T08:30:00.000Z") class TestSlackSubscriptionsTasks(APIBaseTest): subscription: Subscription dashboard: Dashboard diff --git a/ee/tasks/test/subscriptions/test_subscriptions.py b/ee/tasks/test/subscriptions/test_subscriptions.py index 3a63e27ee6ff0..d6afe50b68f7f 100644 --- a/ee/tasks/test/subscriptions/test_subscriptions.py +++ b/ee/tasks/test/subscriptions/test_subscriptions.py @@ -51,17 +51,18 @@ def test_subscription_delivery_scheduling( mock_send_email: MagicMock, mock_send_slack: MagicMock, ) -> None: - subscriptions = [ - create_subscription(team=self.team, insight=self.insight, created_by=self.user), - create_subscription(team=self.team, insight=self.insight, created_by=self.user), - create_subscription(team=self.team, dashboard=self.dashboard, created_by=self.user), - create_subscription( - team=self.team, - dashboard=self.dashboard, - created_by=self.user, - deleted=True, - ), - ] + with freeze_time("2022-02-02T08:30:00.000Z"): # Create outside of buffer before running + subscriptions = [ + create_subscription(team=self.team, insight=self.insight, created_by=self.user), + create_subscription(team=self.team, insight=self.insight, created_by=self.user), + create_subscription(team=self.team, dashboard=self.dashboard, created_by=self.user), + create_subscription( + team=self.team, + dashboard=self.dashboard, + created_by=self.user, + deleted=True, + ), + ] # Modify a subscription to have its target time at least an hour ahead subscriptions[2].start_date = datetime(2022, 1, 1, 10, 0).replace(tzinfo=ZoneInfo("UTC")) subscriptions[2].save() diff --git a/frontend/__snapshots__/lemon-ui-icons--shelf-p--dark.png b/frontend/__snapshots__/lemon-ui-icons--shelf-p--dark.png index 0a34688676935..67a4ddf4b3e63 100644 Binary files a/frontend/__snapshots__/lemon-ui-icons--shelf-p--dark.png and b/frontend/__snapshots__/lemon-ui-icons--shelf-p--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-icons--shelf-p--light.png b/frontend/__snapshots__/lemon-ui-icons--shelf-p--light.png index b2ee6861ef3c7..b9e7a5f5ac4af 100644 Binary files a/frontend/__snapshots__/lemon-ui-icons--shelf-p--light.png and b/frontend/__snapshots__/lemon-ui-icons--shelf-p--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-icons--shelf-s--dark.png b/frontend/__snapshots__/lemon-ui-icons--shelf-s--dark.png index c4542b66869a0..e329d5748af51 100644 Binary files a/frontend/__snapshots__/lemon-ui-icons--shelf-s--dark.png and b/frontend/__snapshots__/lemon-ui-icons--shelf-s--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-icons--shelf-s--light.png b/frontend/__snapshots__/lemon-ui-icons--shelf-s--light.png index da823d4180f5d..b475f7bcf8a59 100644 Binary files a/frontend/__snapshots__/lemon-ui-icons--shelf-s--light.png and b/frontend/__snapshots__/lemon-ui-icons--shelf-s--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-tabs--default--dark.png b/frontend/__snapshots__/lemon-ui-lemon-tabs--default--dark.png new file mode 100644 index 0000000000000..b79154a5689f7 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-tabs--default--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-tabs--default--light.png b/frontend/__snapshots__/lemon-ui-lemon-tabs--default--light.png new file mode 100644 index 0000000000000..6a788a1681776 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-tabs--default--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-tabs--small--dark.png b/frontend/__snapshots__/lemon-ui-lemon-tabs--small--dark.png new file mode 100644 index 0000000000000..a2f359839fb61 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-tabs--small--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-tabs--small--light.png b/frontend/__snapshots__/lemon-ui-lemon-tabs--small--light.png new file mode 100644 index 0000000000000..27ffbe5d2758e Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-tabs--small--light.png differ diff --git a/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.scss b/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.scss index 55bd11ccb5e25..8f05eca3247b7 100644 --- a/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.scss +++ b/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.scss @@ -1,6 +1,6 @@ .LemonTabs { - --lemon-tabs-margin-bottom: 1rem; --lemon-tabs-gap: 2rem; + --lemon-tabs-margin-bottom: 1rem; --lemon-tabs-content-padding: 0.75rem 0; position: relative; @@ -8,6 +8,12 @@ flex-direction: column; align-self: stretch; + &--small { + --lemon-tabs-gap: 1rem; + --lemon-tabs-margin-bottom: 0.5rem; + --lemon-tabs-content-padding: 0.375rem 0; + } + .Navigation3000__scene > &:first-child, .Navigation3000__scene > :first-child > &:first-child { margin-top: -0.75rem; diff --git a/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.stories.tsx b/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.stories.tsx index 9618ab6215934..78e8e8b838757 100644 --- a/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.stories.tsx @@ -50,5 +50,8 @@ const Template: StoryFn = (props) => { return setActiveKey(newValue)} /> } -export const LemonTabs: Story = Template.bind({}) -LemonTabs.args = {} +export const Default: Story = Template.bind({}) +Default.args = {} + +export const Small: Story = Template.bind({}) +Small.args = { size: 'small' } diff --git a/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.tsx b/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.tsx index 2d7475a415216..e0cd244fbb705 100644 --- a/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.tsx +++ b/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.tsx @@ -28,6 +28,7 @@ export interface LemonTabsProps { onChange?: (key: T) => void /** List of tabs. Falsy entries are ignored - they're there to make conditional tabs convenient. */ tabs: (LemonTab | null | false)[] + size?: 'small' | 'medium' 'data-attr'?: string } @@ -40,6 +41,7 @@ export function LemonTabs({ activeKey, onChange, tabs, + size = 'medium', 'data-attr': dataAttr, }: LemonTabsProps): JSX.Element { const { containerRef, selectionRef, sliderWidth, sliderOffset, transitioning } = useSliderPositioning< @@ -53,7 +55,7 @@ export function LemonTabs({ return (
- - - - ) -} - export function IconPlayCircle(props: LemonIconProps): JSX.Element { return ( @@ -1257,14 +1240,6 @@ export function IconPlayCircle(props: LemonIconProps): JSX.Element { ) } -export function IconPause(props: LemonIconProps): JSX.Element { - return ( - - - - ) -} - export function IconSkipBackward(props: LemonIconProps): JSX.Element { return ( diff --git a/frontend/src/queries/QueryEditor/QueryEditor.tsx b/frontend/src/queries/QueryEditor/QueryEditor.tsx index f6622144372e3..dc7b44eb22e12 100644 --- a/frontend/src/queries/QueryEditor/QueryEditor.tsx +++ b/frontend/src/queries/QueryEditor/QueryEditor.tsx @@ -3,8 +3,10 @@ import clsx from 'clsx' import { useActions, useValues } from 'kea' import { CodeEditor } from 'lib/components/CodeEditors' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { Link } from 'lib/lemon-ui/Link' import { useEffect, useState } from 'react' import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' +import { urls } from 'scenes/urls' import { queryEditorLogic } from '~/queries/QueryEditor/queryEditorLogic' import schema from '~/queries/schema.json' @@ -45,7 +47,10 @@ export function QueryEditor(props: QueryEditorProps): JSX.Element { <> {props.context?.showQueryHelp ? (
-
Insight configurations follow a declarative schema. Edit them as code here.
+
+ Insight configurations follow a declarative schema. Edit them as code here. Open under{' '} + /debug. +
) : null}
( } } -const SYNC_ONLY_QUERY_KINDS = ['HogQLMetadata', 'EventsQuery', 'HogQLAutocomplete'] satisfies NodeKind[keyof NodeKind][] +const SYNC_ONLY_QUERY_KINDS = [ + 'HogQLMetadata', + 'EventsQuery', + 'HogQLAutocomplete', + 'DatabaseSchemaQuery', +] satisfies NodeKind[keyof NodeKind][] /** * Execute a query node and return the response, use async query if enabled diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 7be6c6a2483ef..478f0707d1abf 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -50,6 +50,10 @@ "math_property": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "name": { "type": "string" }, @@ -84,6 +88,10 @@ "limit": { "type": "integer" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "offset": { "type": "integer" }, @@ -151,6 +159,9 @@ "missing_actors_count": { "type": "integer" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "offset": { "type": "integer" }, @@ -580,6 +591,10 @@ "kind": { "$ref": "#/definitions/NodeKind" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "response": { "description": "Cached query response", "type": "object" @@ -841,6 +856,10 @@ "math_property": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "name": { "type": "string" }, @@ -918,6 +937,10 @@ "const": "DatabaseSchemaQuery", "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "response": { "$ref": "#/definitions/DatabaseSchemaQueryResponse", "description": "Cached query response" @@ -1100,6 +1123,10 @@ "math_property": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "name": { "type": "string" }, @@ -1293,6 +1320,10 @@ "math_property": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "name": { "type": "string" }, @@ -1369,6 +1400,10 @@ "description": "Number of rows to return", "type": "integer" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "offset": { "description": "Number of rows to skip before returning rows", "type": "integer" @@ -1457,6 +1492,9 @@ "limit": { "type": "integer" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "offset": { "type": "integer" }, @@ -1648,6 +1686,9 @@ "const": "FunnelCorrelationActorsQuery", "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "response": { "$ref": "#/definitions/ActorsQueryResponse" }, @@ -1724,6 +1765,9 @@ "limit": { "type": "integer" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "offset": { "type": "integer" }, @@ -1829,6 +1873,10 @@ "math_property": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "name": { "type": "string" }, @@ -1906,6 +1954,10 @@ "math_property": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "name": { "type": "string" }, @@ -2094,6 +2146,9 @@ "const": "FunnelsActorsQuery", "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "response": { "$ref": "#/definitions/ActorsQueryResponse" }, @@ -2244,6 +2299,10 @@ "const": "FunnelsQuery", "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "properties": { "anyOf": [ { @@ -2285,6 +2344,9 @@ "last_refresh": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "next_allowed_client_refresh": { "type": "string" }, @@ -2385,6 +2447,10 @@ "const": "HogQLAutocomplete", "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "response": { "$ref": "#/definitions/HogQLAutocompleteResponse", "description": "Cached query response" @@ -2474,6 +2540,10 @@ "const": "HogQLMetadata", "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "response": { "$ref": "#/definitions/HogQLMetadataResponse", "description": "Cached query response" @@ -2580,7 +2650,8 @@ "type": "string" }, "modifiers": { - "$ref": "#/definitions/HogQLQueryModifiers" + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" }, "query": { "type": "string" @@ -2737,6 +2808,9 @@ "const": "InsightActorsQuery", "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "response": { "$ref": "#/definitions/ActorsQueryResponse" }, @@ -2759,6 +2833,9 @@ "includeRecordings": { "type": "boolean" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "response": { "$ref": "#/definitions/ActorsQueryResponse" } @@ -3064,6 +3141,10 @@ "kind": { "$ref": "#/definitions/NodeKind" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "properties": { "anyOf": [ { @@ -3144,6 +3225,10 @@ "$ref": "#/definitions/LifecycleFilter", "description": "Properties specific to the lifecycle insight" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "properties": { "anyOf": [ { @@ -3188,6 +3273,9 @@ "last_refresh": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "next_allowed_client_refresh": { "type": "string" }, @@ -3439,6 +3527,10 @@ "const": "PathsQuery", "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "pathsFilter": { "$ref": "#/definitions/PathsFilter", "description": "Properties specific to the paths insight" @@ -3480,6 +3572,9 @@ "last_refresh": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "next_allowed_client_refresh": { "type": "string" }, @@ -3547,6 +3642,10 @@ "limit": { "type": "integer" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "offset": { "type": "integer" }, @@ -3707,6 +3806,9 @@ "last_refresh": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "next_allowed_client_refresh": { "type": "string" }, @@ -3771,6 +3873,9 @@ "limit": { "type": "integer" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "offset": { "type": "integer" }, @@ -3816,6 +3921,9 @@ "missing_actors_count": { "type": "integer" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "offset": { "type": "integer" }, @@ -4128,6 +4236,9 @@ "last_refresh": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "next_allowed_client_refresh": { "type": "string" }, @@ -4172,6 +4283,9 @@ "limit": { "type": "integer" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "next_allowed_client_refresh": { "type": "string" }, @@ -4215,6 +4329,9 @@ "last_refresh": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "next_allowed_client_refresh": { "type": "string" }, @@ -4251,6 +4368,9 @@ "last_refresh": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "next_allowed_client_refresh": { "type": "string" }, @@ -4282,6 +4402,9 @@ "last_refresh": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "next_allowed_client_refresh": { "type": "string" }, @@ -4313,6 +4436,9 @@ "last_refresh": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "next_allowed_client_refresh": { "type": "string" }, @@ -4344,6 +4470,9 @@ "last_refresh": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "next_allowed_client_refresh": { "type": "string" }, @@ -4379,6 +4508,9 @@ "limit": { "type": "integer" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "offset": { "type": "integer" }, @@ -4692,6 +4824,10 @@ "const": "RetentionQuery", "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "properties": { "anyOf": [ { @@ -4733,6 +4869,9 @@ "last_refresh": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "next_allowed_client_refresh": { "type": "string" }, @@ -4965,6 +5104,10 @@ "const": "SessionsTimelineQuery", "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "personId": { "description": "Fetch sessions only for a given person", "type": "string" @@ -5074,6 +5217,10 @@ "const": "StickinessQuery", "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "properties": { "anyOf": [ { @@ -5119,6 +5266,9 @@ "last_refresh": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "next_allowed_client_refresh": { "type": "string" }, @@ -5169,6 +5319,10 @@ "const": "TimeToSeeDataQuery", "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "response": { "description": "Cached query response", "type": "object" @@ -5203,6 +5357,10 @@ "const": "TimeToSeeDataSessionsQuery", "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "response": { "$ref": "#/definitions/TimeToSeeDataSessionsQueryResponse", "description": "Cached query response" @@ -5393,6 +5551,10 @@ "const": "TrendsQuery", "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, "properties": { "anyOf": [ { @@ -5441,6 +5603,9 @@ "last_refresh": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "next_allowed_client_refresh": { "type": "string" }, @@ -5516,6 +5681,9 @@ "dateRange": { "$ref": "#/definitions/DateRange" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "properties": { "$ref": "#/definitions/WebAnalyticsPropertyFilters" }, @@ -5577,6 +5745,9 @@ "const": "WebOverviewQuery", "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "properties": { "$ref": "#/definitions/WebAnalyticsPropertyFilters" }, @@ -5614,6 +5785,9 @@ "last_refresh": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "next_allowed_client_refresh": { "type": "string" }, @@ -5681,6 +5855,9 @@ "limit": { "type": "integer" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "properties": { "$ref": "#/definitions/WebAnalyticsPropertyFilters" }, @@ -5728,6 +5905,9 @@ "limit": { "type": "integer" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "next_allowed_client_refresh": { "type": "string" }, @@ -5765,6 +5945,9 @@ "const": "WebTopClicksQuery", "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "properties": { "$ref": "#/definitions/WebAnalyticsPropertyFilters" }, @@ -5806,6 +5989,9 @@ "last_refresh": { "type": "string" }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers" + }, "next_allowed_client_refresh": { "type": "string" }, diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 72941d8436e74..5c991e3fb1d9c 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -175,6 +175,8 @@ export type AnyResponseType = export interface DataNode extends Node { /** Cached query response */ response?: Record + /** Modifiers used when performing the query */ + modifiers?: HogQLQueryModifiers } /** HogQL Query Options are automatically set per team. However, they can be overriden in the query. */ @@ -238,7 +240,6 @@ export interface HogQLQuery extends DataNode { filters?: HogQLFilters /** Constant values that can be referenced with the {placeholder} syntax in the query */ values?: Record - modifiers?: HogQLQueryModifiers explain?: boolean response?: HogQLQueryResponse } @@ -416,6 +417,7 @@ export interface EventsQueryResponse { timings?: QueryTiming[] limit?: integer offset?: integer + modifiers?: HogQLQueryModifiers } export interface EventsQueryPersonColumn { uuid: string @@ -628,6 +630,8 @@ export interface InsightsQueryBase extends Node { aggregation_group_type_index?: integer /** Sampling rate */ samplingFactor?: number | null + /** Modifiers used when performing the query */ + modifiers?: HogQLQueryModifiers } /** `TrendsFilterType` minus everything inherited from `FilterType` and @@ -892,6 +896,7 @@ export interface QueryResponse { is_cached?: boolean last_refresh?: string next_allowed_client_refresh?: string + modifiers?: HogQLQueryModifiers } export type QueryStatus = { @@ -940,6 +945,7 @@ export interface ActorsQueryResponse { limit: integer offset: integer missing_actors_count?: integer + modifiers?: HogQLQueryModifiers } export interface ActorsQuery extends DataNode { @@ -991,6 +997,7 @@ export interface WebAnalyticsQueryBase { forceSamplingRate?: SamplingRate } useSessionsTable?: boolean + modifiers?: HogQLQueryModifiers } export interface WebOverviewQuery extends WebAnalyticsQueryBase { @@ -1100,6 +1107,7 @@ export type Day = integer export interface InsightActorsQueryBase { includeRecordings?: boolean response?: ActorsQueryResponse + modifiers?: HogQLQueryModifiers } export interface InsightActorsQuery extends InsightActorsQueryBase { kind: NodeKind.InsightActorsQuery @@ -1165,6 +1173,7 @@ export interface FunnelCorrelationResponse { hasMore?: boolean limit?: integer offset?: integer + modifiers?: HogQLQueryModifiers } export enum FunnelCorrelationResultsType { diff --git a/frontend/src/scenes/data-warehouse/new/sourceWizardLogic.tsx b/frontend/src/scenes/data-warehouse/new/sourceWizardLogic.tsx index f41a90e086fb7..a99f597d5a43e 100644 --- a/frontend/src/scenes/data-warehouse/new/sourceWizardLogic.tsx +++ b/frontend/src/scenes/data-warehouse/new/sourceWizardLogic.tsx @@ -427,6 +427,7 @@ export const sourceWizardLogic = kea([ actions.onClear() actions.clearSource() actions.loadSources(null) + actions.resetSourceConnectionDetails() router.actions.push(urls.dataWarehouseSettings()) }, cancelWizard: () => { diff --git a/frontend/src/scenes/debug/DebugScene.tsx b/frontend/src/scenes/debug/DebugScene.tsx index 9af6753578fbc..fffbe0dc4e521 100644 --- a/frontend/src/scenes/debug/DebugScene.tsx +++ b/frontend/src/scenes/debug/DebugScene.tsx @@ -1,14 +1,20 @@ import { useActions, useValues } from 'kea' +import { CodeEditor } from 'lib/components/CodeEditors' import { PageHeader } from 'lib/components/PageHeader' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' import { LemonSelect } from 'lib/lemon-ui/LemonSelect' import { HogQLDebug } from 'scenes/debug/HogQLDebug' +import { Modifiers } from 'scenes/debug/Modifiers' import { SceneExport } from 'scenes/sceneTypes' import { stringifiedExamples } from '~/queries/examples' +import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' import { Query } from '~/queries/Query/Query' -import { HogQLQuery } from '~/queries/schema' +import { QueryEditor } from '~/queries/QueryEditor/QueryEditor' +import { DataNode, HogQLQuery, Node } from '~/queries/schema' +import { isDataTableNode, isInsightVizNode } from '~/queries/utils' import { debugSceneLogic } from './debugSceneLogic' @@ -18,12 +24,20 @@ interface QueryDebugProps { setQuery: (query: string) => void } function QueryDebug({ query, setQuery, queryKey }: QueryDebugProps): JSX.Element { - let parsed: Record | undefined + let parsed: Record | null = null try { parsed = JSON.parse(query) } catch (e) { // do nothing } + + const dataNodeLogicProps: DataNodeLogicProps = { + query: (parsed ?? {}) as DataNode, + key: queryKey, + dataNodeCollectionId: queryKey, + } + const { response } = useValues(dataNodeLogic(dataNodeLogicProps)) + return ( <> {parsed && parsed?.kind === 'HogQLQuery' ? ( @@ -33,13 +47,32 @@ function QueryDebug({ query, setQuery, queryKey }: QueryDebugProps): JSX.Element setQuery={(query) => setQuery(JSON.stringify(query, null, 2))} /> ) : ( - setQuery(JSON.stringify(query, null, 2))} - context={{ - showQueryEditor: true, - }} - /> +
+ + setQuery(JSON.stringify({ ...parsed, source: query }, null, 2)) + : (query) => setQuery(JSON.stringify(query, null, 2)) + } + query={parsed?.source ?? parsed} + response={response} + /> + + setQuery(JSON.stringify(query, null, 2))} + /> + {response && parsed && (isDataTableNode(parsed as Node) || isInsightVizNode(parsed as Node)) ? ( + + ) : null} +
)} ) diff --git a/frontend/src/scenes/debug/HogQLDebug.tsx b/frontend/src/scenes/debug/HogQLDebug.tsx index 62e36a9bbd369..d42f4af5d5664 100644 --- a/frontend/src/scenes/debug/HogQLDebug.tsx +++ b/frontend/src/scenes/debug/HogQLDebug.tsx @@ -1,9 +1,8 @@ import { BindLogic, useValues } from 'kea' import { CodeEditor } from 'lib/components/CodeEditors' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { LemonLabel } from 'lib/lemon-ui/LemonLabel' -import { LemonSelect } from 'lib/lemon-ui/LemonSelect' import { LemonTable } from 'lib/lemon-ui/LemonTable' +import { Modifiers } from 'scenes/debug/Modifiers' import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' import { DateRange } from '~/queries/nodes/DataNode/DateRange' @@ -62,87 +61,7 @@ export function HogQLDebug({ query, setQuery, queryKey }: HogQLDebugProps): JSX.
-
- - POE: - - setQuery({ - ...query, - modifiers: { ...query.modifiers, personsOnEventsMode: value }, - } as HogQLQuery) - } - value={query.modifiers?.personsOnEventsMode ?? response?.modifiers?.personsOnEventsMode} - /> - - - Persons ArgMax: - - setQuery({ - ...query, - modifiers: { ...query.modifiers, personsArgMaxVersion: value }, - } as HogQLQuery) - } - value={query.modifiers?.personsArgMaxVersion ?? response?.modifiers?.personsArgMaxVersion} - /> - - - In Cohort Via: - - setQuery({ - ...query, - modifiers: { ...query.modifiers, inCohortVia: value }, - } as HogQLQuery) - } - value={query.modifiers?.inCohortVia ?? response?.modifiers?.inCohortVia} - /> - - - Materialization Mode: - - setQuery({ - ...query, - modifiers: { ...query.modifiers, materializationMode: value }, - } as HogQLQuery) - } - value={query.modifiers?.materializationMode ?? response?.modifiers?.materializationMode} - /> - -
+ {dataLoading ? ( <>

Running query...

diff --git a/frontend/src/scenes/debug/Modifiers.tsx b/frontend/src/scenes/debug/Modifiers.tsx new file mode 100644 index 0000000000000..ea6551ef65f6c --- /dev/null +++ b/frontend/src/scenes/debug/Modifiers.tsx @@ -0,0 +1,99 @@ +import { LemonLabel } from 'lib/lemon-ui/LemonLabel' +import { LemonSelect } from 'lib/lemon-ui/LemonSelect' + +import { DataNode, HogQLQuery, HogQLQueryResponse } from '~/queries/schema' + +export interface ModifiersProps { + setQuery: (query: DataNode) => void + query: HogQLQuery | Record | null + response: HogQLQueryResponse | null +} + +export function Modifiers({ setQuery, query, response = null }: ModifiersProps): JSX.Element | null { + if (query === null) { + return null + } + return ( +
+ + POE: + + setQuery({ + ...query, + modifiers: { ...query.modifiers, personsOnEventsMode: value }, + } as HogQLQuery) + } + value={query.modifiers?.personsOnEventsMode ?? response?.modifiers?.personsOnEventsMode} + /> + + + Persons ArgMax: + + setQuery({ + ...query, + modifiers: { ...query.modifiers, personsArgMaxVersion: value }, + } as HogQLQuery) + } + value={query.modifiers?.personsArgMaxVersion ?? response?.modifiers?.personsArgMaxVersion} + /> + + + In Cohort Via: + + setQuery({ + ...query, + modifiers: { ...query.modifiers, inCohortVia: value }, + } as HogQLQuery) + } + value={query.modifiers?.inCohortVia ?? response?.modifiers?.inCohortVia} + /> + + + Materialization Mode: + + setQuery({ + ...query, + modifiers: { ...query.modifiers, materializationMode: value }, + } as HogQLQuery) + } + value={query.modifiers?.materializationMode ?? response?.modifiers?.materializationMode} + /> + +
+ ) +} diff --git a/frontend/src/scenes/insights/EditorFilters/FunnelsQuerySteps.tsx b/frontend/src/scenes/insights/EditorFilters/FunnelsQuerySteps.tsx index 7f87def330757..c21895d997a69 100644 --- a/frontend/src/scenes/insights/EditorFilters/FunnelsQuerySteps.tsx +++ b/frontend/src/scenes/insights/EditorFilters/FunnelsQuerySteps.tsx @@ -54,7 +54,7 @@ export function FunnelsQuerySteps({ insightProps }: EditorFilterProps): JSX.Elem bordered filters={actionFilters} setFilters={setActionFilters} - typeKey={`${keyForInsightLogicProps('new')(insightProps)}-FunnelsQuerySteps`} + typeKey={keyForInsightLogicProps('new')(insightProps)} mathAvailability={MathAvailability.None} hideDeleteBtn={filterSteps.length === 1} buttonCopy="Add step" diff --git a/frontend/src/scenes/session-recordings/player/controller/PlayerController.tsx b/frontend/src/scenes/session-recordings/player/controller/PlayerController.tsx index df5bf7e2a280e..77a0374e70904 100644 --- a/frontend/src/scenes/session-recordings/player/controller/PlayerController.tsx +++ b/frontend/src/scenes/session-recordings/player/controller/PlayerController.tsx @@ -1,8 +1,8 @@ -import { IconPlay } from '@posthog/icons' -import { LemonMenu } from '@posthog/lemon-ui' +import { IconFastForward, IconPause, IconPlay } from '@posthog/icons' +import { LemonMenu, LemonSwitch } from '@posthog/lemon-ui' import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { IconFullScreen, IconPause, IconSkipInactivity } from 'lib/lemon-ui/icons' +import { IconFullScreen } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { @@ -60,26 +60,20 @@ export function PlayerController(): JSX.Element {
- { - setSkipInactivitySetting(!skipInactivitySetting) - }} - icon={ - } - > - - {skipInactivitySetting ? 'Skipping inactivity' : 'Skip inactivity'} - - + />
diff --git a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorControls.tsx b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorControls.tsx index 2f717c954008b..4724c1f58548e 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorControls.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorControls.tsx @@ -1,5 +1,5 @@ import { IconBug, IconDashboard, IconInfo, IconPause, IconTerminal, IconX } from '@posthog/icons' -import { LemonButton, LemonCheckbox, LemonInput, LemonSelect, Tooltip } from '@posthog/lemon-ui' +import { LemonButton, LemonCheckbox, LemonInput, LemonSelect, LemonTabs, Tooltip } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { FEATURE_FLAGS } from 'lib/constants' import { IconPlayCircle, IconUnverifiedEvent } from 'lib/lemon-ui/icons' @@ -39,25 +39,30 @@ function TabButtons({ const { setTab } = useActions(inspectorLogic) return ( - <> - {tabs.map((tabId) => { + setTab(tabId)} + tabs={tabs.map((tabId) => { const TabIcon = TabToIcon[tabId] - return ( - : : undefined - } - active={tab === tabId} - onClick={() => setTab(tabId)} - > - {capitalizeFirstLetter(tabId)} - - ) + + return { + key: tabId, + label: ( +
+ {TabIcon ? ( + tabsState[tabId] === 'loading' ? ( + + ) : ( + + ) + ) : undefined} + {capitalizeFirstLetter(tabId)} +
+ ), + } })} - + /> ) } @@ -96,130 +101,136 @@ export function PlayerInspectorControls({ onClose }: { onClose: () => void }): J } return ( -
-
-
- +
+
+
+ +
+ } onClick={onClose} />
- } onClick={onClose} /> -
- -
- {miniFilters.map((filter) => ( - { - // "alone" should always be a select-to-true action - setMiniFilter(filter.key, filter.alone || !filter.enabled) - }} - tooltip={filter.tooltip} - > - {filter.name} - - ))}
-
-
-
- +
+ {miniFilters.map((filter) => ( + setSearchQuery(e)} - placeholder="Search..." - type="search" - value={searchQuery} - fullWidth - suffix={ - }> + noPadding + active={filter.enabled} + onClick={() => { + // "alone" should always be a select-to-true action + setMiniFilter(filter.key, filter.alone || !filter.enabled) + }} + tooltip={filter.tooltip} + > + {filter.name} + + ))} +
+ +
+
+
+ setSearchQuery(e)} + placeholder="Search..." + type="search" + value={searchQuery} + fullWidth + suffix={ + }> + + + } + /> +
+ + {windowIds.length > 1 ? ( +
+ setWindowIdFilter(val || null)} + options={[ + { + value: null, + label: 'All windows', + icon: , + }, + ...windowIds.map((windowId, index) => ({ + value: windowId, + label: `Window ${index + 1}`, + icon: , + })), + ]} + /> + - } - /> +
+ ) : null}
- {windowIds.length > 1 ? ( -
- setWindowIdFilter(val || null)} - options={[ - { - value: null, - label: 'All windows', - icon: , - }, - ...windowIds.map((windowId, index) => ({ - value: windowId, - label: `Window ${index + 1}`, - icon: , - })), - ]} - /> +
+ { + // 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 setting + setSyncScroll(!syncScroll) + } + }} + tooltipPlacement="left" + tooltip={ + syncScroll && syncScrollingPaused + ? 'Synced scrolling is paused - click to resume' + : 'Scroll the list in sync with the recording playback' + } + > + {syncScroll && syncScrollingPaused ? ( + + ) : ( + + )} + +
+
+ {showMatchingEventsFilter ? ( +
+ + Only events matching filters -
- ) : null} -
+ -
- { - // 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 setting - setSyncScroll(!syncScroll) - } - }} - tooltipPlacement="left" - tooltip={ - syncScroll && syncScrollingPaused - ? 'Synced scrolling is paused - click to resume' - : 'Scroll the list in sync with the recording playback' - } - > - {syncScroll && syncScrollingPaused ? ( - - ) : ( - - )} - -
+ +
+ ) : null}
- {showMatchingEventsFilter ? ( -
- - Only events matching filters - - - - - - -
- ) : null}
) } diff --git a/mypy-baseline.txt b/mypy-baseline.txt index 713cf17200a43..58a2acbea7c86 100644 --- a/mypy-baseline.txt +++ b/mypy-baseline.txt @@ -133,7 +133,6 @@ posthog/hogql_queries/legacy_compatibility/filter_to_query.py:0: error: Dict ent posthog/hogql_queries/legacy_compatibility/filter_to_query.py:0: error: Dict entry 0 has incompatible type "str": "LifecycleFilter"; expected "str": "TrendsFilter" [dict-item] posthog/hogql_queries/legacy_compatibility/filter_to_query.py:0: error: Dict entry 0 has incompatible type "str": "StickinessFilter"; expected "str": "TrendsFilter" [dict-item] posthog/hogql_queries/legacy_compatibility/feature_flag.py:0: error: Item "AnonymousUser" of "User | AnonymousUser" has no attribute "email" [union-attr] -posthog/hogql/modifiers.py:0: error: Incompatible types in assignment (expression has type "PersonOnEventsMode", variable has type "PersonsOnEventsMode | None") [assignment] posthog/api/utils.py:0: error: Incompatible types in assignment (expression has type "type[EventDefinition]", variable has type "type[EnterpriseEventDefinition]") [assignment] posthog/api/utils.py:0: error: Argument 1 to "UUID" has incompatible type "int | str"; expected "str | None" [arg-type] ee/billing/quota_limiting.py:0: error: List comprehension has incompatible type List[int]; expected List[str] [misc] @@ -276,16 +275,9 @@ posthog/hogql/printer.py:0: error: Name "args" already defined on line 0 [no-re posthog/hogql/printer.py:0: error: Name "args" already defined on line 0 [no-redef] posthog/hogql/printer.py:0: error: Argument 1 to "lookup_field_by_name" has incompatible type "SelectQueryType | None"; expected "SelectQueryType" [arg-type] posthog/hogql/printer.py:0: error: Item "TableType" of "TableType | TableAliasType" has no attribute "alias" [union-attr] -posthog/hogql/printer.py:0: error: Non-overlapping equality check (left operand type: "PersonsOnEventsMode | None", right operand type: "Literal[PersonOnEventsMode.DISABLED]") [comparison-overlap] -posthog/hogql/printer.py:0: error: Statement is unreachable [unreachable] posthog/hogql/printer.py:0: error: "FieldOrTable" has no attribute "name" [attr-defined] -posthog/hogql/printer.py:0: error: Non-overlapping equality check (left operand type: "PersonsOnEventsMode | None", right operand type: "Literal[PersonOnEventsMode.DISABLED]") [comparison-overlap] -posthog/hogql/printer.py:0: error: Statement is unreachable [unreachable] posthog/hogql/printer.py:0: error: "FieldOrTable" has no attribute "name" [attr-defined] posthog/hogql/printer.py:0: error: Argument 2 to "_get_materialized_column" of "_Printer" has incompatible type "str | int"; expected "str" [arg-type] -posthog/hogql/printer.py:0: error: Non-overlapping equality check (left operand type: "PersonsOnEventsMode | None", right operand type: "Literal[PersonOnEventsMode.DISABLED]") [comparison-overlap] -posthog/hogql/printer.py:0: error: Argument 2 to "_get_materialized_column" of "_Printer" has incompatible type "str | int"; expected "str" [arg-type] -posthog/hogql/printer.py:0: error: Statement is unreachable [unreachable] posthog/hogql/printer.py:0: error: Argument 1 to "_print_identifier" of "_Printer" has incompatible type "str | None"; expected "str" [arg-type] posthog/api/organization.py:0: error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases [misc] posthog/api/organization.py:0: error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases [misc] @@ -309,7 +301,6 @@ posthog/hogql/filters.py:0: note: PEP 484 prohibits implicit Optional. According posthog/hogql/filters.py:0: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase posthog/hogql/query.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "str | SelectQuery | SelectUnionQuery") [assignment] posthog/hogql/query.py:0: error: Incompatible types in assignment (expression has type "Expr", variable has type "SelectQuery | SelectUnionQuery") [assignment] -posthog/hogql/query.py:0: error: Incompatible types in assignment (expression has type "Expr", variable has type "SelectQuery | SelectUnionQuery") [assignment] posthog/hogql/query.py:0: error: Argument 1 to "get_default_limit_for_context" has incompatible type "LimitContext | None"; expected "LimitContext" [arg-type] posthog/hogql/query.py:0: error: "SelectQuery" has no attribute "select_queries" [attr-defined] posthog/hogql/query.py:0: error: Subclass of "SelectQuery" and "SelectUnionQuery" cannot exist: would have incompatible method signatures [unreachable] @@ -330,12 +321,9 @@ posthog/hogql_queries/insights/trends/breakdown.py:0: error: Item "None" of "Bre 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] posthog/hogql_queries/insights/trends/breakdown.py:0: error: Item "None" of "BreakdownFilter | None" has no attribute "breakdown_group_type_index" [union-attr] posthog/hogql_queries/hogql_query_runner.py:0: error: Statement is unreachable [unreachable] -posthog/hogql_queries/hogql_query_runner.py:0: error: Argument "placeholders" to "parse_select" has incompatible type "dict[str, Constant] | None"; expected "dict[str, Expr] | None" [arg-type] -posthog/hogql_queries/hogql_query_runner.py:0: error: Incompatible types in assignment (expression has type "Expr", variable has type "SelectQuery | SelectUnionQuery") [assignment] posthog/hogql_queries/hogql_query_runner.py:0: error: Incompatible return value type (got "SelectQuery | SelectUnionQuery", expected "SelectQuery") [return-value] posthog/hogql_queries/events_query_runner.py:0: error: Statement is unreachable [unreachable] posthog/hogql/metadata.py:0: error: Argument "metadata_source" to "translate_hogql" has incompatible type "SelectQuery | SelectUnionQuery"; expected "SelectQuery | None" [arg-type] -posthog/hogql/metadata.py:0: error: Incompatible types in assignment (expression has type "Expr", variable has type "SelectQuery | SelectUnionQuery") [assignment] posthog/queries/breakdown_props.py:0: error: Argument 1 to "translate_hogql" has incompatible type "str | int"; expected "str" [arg-type] posthog/queries/funnels/base.py:0: error: "HogQLContext" has no attribute "person_on_events_mode" [attr-defined] posthog/queries/funnels/base.py:0: error: Argument 1 to "translate_hogql" has incompatible type "str | int"; expected "str" [arg-type] @@ -422,8 +410,6 @@ posthog/session_recordings/queries/session_recording_list_from_replay_summary.py posthog/session_recordings/queries/session_recording_list_from_replay_summary.py:0: note: If the method is meant to be abstract, use @abc.abstractmethod posthog/session_recordings/queries/session_recording_list_from_replay_summary.py:0: error: Missing return statement [empty-body] posthog/session_recordings/queries/session_recording_list_from_replay_summary.py:0: note: If the method is meant to be abstract, use @abc.abstractmethod -posthog/session_recordings/queries/session_recording_list_from_replay_summary.py:0: error: Incompatible types in assignment (expression has type "PersonOnEventsMode", variable has type "PersonsOnEventsMode | None") [assignment] -posthog/session_recordings/queries/session_recording_list_from_replay_summary.py:0: error: Incompatible types in assignment (expression has type "PersonOnEventsMode", variable has type "PersonsOnEventsMode | None") [assignment] posthog/hogql_queries/test/test_query_runner.py:0: error: Variable "TestQueryRunner" is not valid as a type [valid-type] posthog/hogql_queries/test/test_query_runner.py:0: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases posthog/hogql_queries/test/test_query_runner.py:0: error: Invalid base class "TestQueryRunner" [misc] diff --git a/package.json b/package.json index 4fd1464b3f4b1..5c160594e4a43 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@medv/finder": "^3.1.0", "@microlink/react-json-view": "^1.21.3", "@monaco-editor/react": "4.4.6", - "@posthog/icons": "0.6.7", + "@posthog/icons": "0.7.0", "@posthog/plugin-scaffold": "^1.4.4", "@react-hook/size": "^2.1.2", "@rrweb/types": "2.0.0-alpha.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73e33f970136c..84f402083f4a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,8 +47,8 @@ dependencies: specifier: 4.4.6 version: 4.4.6(monaco-editor@0.39.0)(react-dom@18.2.0)(react@18.2.0) '@posthog/icons': - specifier: 0.6.7 - version: 0.6.7(react-dom@18.2.0)(react@18.2.0) + specifier: 0.7.0 + version: 0.7.0(react-dom@18.2.0)(react@18.2.0) '@posthog/plugin-scaffold': specifier: ^1.4.4 version: 1.4.4 @@ -5186,8 +5186,8 @@ packages: resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==} dev: false - /@posthog/icons@0.6.7(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Jxizmu+fIW6y3kl13oC3avq9YtfRfszmtme75kYFnm+btRGOjwgnTGYPsPCAz9Pw5LsTqii/uNngUsMNotiTZA==} + /@posthog/icons@0.7.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-wl9+On19tm/m7eh0KwogD8Vk4wd5//ewSOfKYhh1Z2kRcJbpWniWWo13rOIno18G5U2+ngaPndxEe2yJMtEDGQ==} peerDependencies: react: '>=16.14.0' react-dom: '>=16.14.0' diff --git a/posthog/api/test/__snapshots__/test_annotation.ambr b/posthog/api/test/__snapshots__/test_annotation.ambr index 043697e9d0d46..af842be0643e0 100644 --- a/posthog/api/test/__snapshots__/test_annotation.ambr +++ b/posthog/api/test/__snapshots__/test_annotation.ambr @@ -364,7 +364,7 @@ OR "posthog_annotation"."team_id" = 2) AND NOT "posthog_annotation"."deleted") ORDER BY "posthog_annotation"."date_marker" DESC - LIMIT 1000 /*controller='project_annotations-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/annotations/%3F%24'*/ + LIMIT 1000 ''' # --- # name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.2 diff --git a/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr b/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr index 2cd5eea5a05f6..3a13d80bf85a7 100644 --- a/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr +++ b/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr @@ -8505,7 +8505,7 @@ FROM "posthog_instancesetting" WHERE "posthog_instancesetting"."key" = 'constance:posthog:PERSON_ON_EVENTS_ENABLED' ORDER BY "posthog_instancesetting"."id" ASC - LIMIT 1 /*controller='project_dashboards-detail',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/%3F%24'*/ + LIMIT 1 ''' # --- # name: TestDashboard.test_loading_individual_dashboard_does_not_prefetch_all_possible_tiles.32 diff --git a/posthog/hogql/errors.py b/posthog/hogql/errors.py index d603e2af74c85..36227fa42638e 100644 --- a/posthog/hogql/errors.py +++ b/posthog/hogql/errors.py @@ -1,4 +1,5 @@ from typing import Optional, TYPE_CHECKING +from abc import ABC if TYPE_CHECKING: from .ast import Expr @@ -6,9 +7,7 @@ # Base -class BaseHogQLError(Exception): - """Base exception for HogQL. These are exposed to the user.""" - +class BaseHogQLError(Exception, ABC): start: Optional[int] end: Optional[int] diff --git a/posthog/hogql/filters.py b/posthog/hogql/filters.py index 78bef13675f70..dcec66efad6e0 100644 --- a/posthog/hogql/filters.py +++ b/posthog/hogql/filters.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Optional, TypeVar from dateutil.parser import isoparse @@ -12,7 +12,10 @@ from posthog.utils import relative_date_parse -def replace_filters(node: ast.Expr, filters: Optional[HogQLFilters], team: Team) -> ast.Expr: +T = TypeVar("T", bound=ast.Expr) + + +def replace_filters(node: T, filters: Optional[HogQLFilters], team: Team) -> T: return ReplaceFilters(filters, team).visit(node) diff --git a/posthog/hogql/modifiers.py b/posthog/hogql/modifiers.py index 3c6fa11a9fc3b..c14e952dcb3c0 100644 --- a/posthog/hogql/modifiers.py +++ b/posthog/hogql/modifiers.py @@ -1,7 +1,12 @@ from typing import Optional, TYPE_CHECKING -from posthog.schema import HogQLQueryModifiers, InCohortVia, MaterializationMode, PersonsArgMaxVersion -from posthog.utils import PersonOnEventsMode +from posthog.schema import ( + HogQLQueryModifiers, + InCohortVia, + MaterializationMode, + PersonsArgMaxVersion, + PersonsOnEventsMode, +) if TYPE_CHECKING: from posthog.models import Team @@ -16,7 +21,7 @@ def create_default_modifiers_for_team( modifiers = modifiers.model_copy() if modifiers.personsOnEventsMode is None: - modifiers.personsOnEventsMode = team.person_on_events_mode or PersonOnEventsMode.DISABLED + modifiers.personsOnEventsMode = team.person_on_events_mode or PersonsOnEventsMode.disabled if modifiers.personsArgMaxVersion is None: modifiers.personsArgMaxVersion = PersonsArgMaxVersion.auto diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 3b6ad33dd876d..3f5be7cc42b83 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -41,8 +41,7 @@ from posthog.models.team.team import WeekStartDay from posthog.models.team import Team from posthog.models.utils import UUIDT -from posthog.schema import HogQLQueryModifiers, InCohortVia, MaterializationMode -from posthog.utils import PersonOnEventsMode +from posthog.schema import HogQLQueryModifiers, InCohortVia, MaterializationMode, PersonsOnEventsMode def team_id_guard_for_table(table_type: Union[ast.TableType, ast.TableAliasType], context: HogQLContext) -> ast.Expr: @@ -954,7 +953,7 @@ def visit_field_type(self, type: ast.FieldType): and type.name == "properties" and type.table_type.field == "poe" ): - if self.context.modifiers.personsOnEventsMode != PersonOnEventsMode.DISABLED: + if self.context.modifiers.personsOnEventsMode != PersonsOnEventsMode.disabled: field_sql = "person_properties" else: field_sql = "person_props" @@ -978,7 +977,7 @@ def visit_field_type(self, type: ast.FieldType): # :KLUDGE: Legacy person properties handling. Only used within non-HogQL queries, such as insights. if self.context.within_non_hogql_query and field_sql == "events__pdi__person.properties": - if self.context.modifiers.personsOnEventsMode != PersonOnEventsMode.DISABLED: + if self.context.modifiers.personsOnEventsMode != PersonsOnEventsMode.disabled: field_sql = "person_properties" else: field_sql = "person_props" @@ -1028,10 +1027,12 @@ def visit_property_type(self, type: ast.PropertyType): or (isinstance(table, ast.VirtualTableType) and table.field == "poe") ): # :KLUDGE: Legacy person properties handling. Only used within non-HogQL queries, such as insights. - if self.context.modifiers.personsOnEventsMode != PersonOnEventsMode.DISABLED: - materialized_column = self._get_materialized_column("events", type.chain[0], "person_properties") + if self.context.modifiers.personsOnEventsMode != PersonsOnEventsMode.disabled: + materialized_column = self._get_materialized_column( + "events", str(type.chain[0]), "person_properties" + ) else: - materialized_column = self._get_materialized_column("person", type.chain[0], "properties") + materialized_column = self._get_materialized_column("person", str(type.chain[0]), "properties") if materialized_column: materialized_property_sql = self._print_identifier(materialized_column) diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index a4d5337071b3d..1a8a2130c5245 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -14,9 +14,8 @@ from posthog.hogql.printer import print_ast, to_printed_hogql from posthog.models import PropertyDefinition from posthog.models.team.team import WeekStartDay -from posthog.schema import HogQLQueryModifiers, PersonsArgMaxVersion +from posthog.schema import HogQLQueryModifiers, PersonsArgMaxVersion, PersonsOnEventsMode from posthog.test.base import BaseTest -from posthog.utils import PersonOnEventsMode class TestPrinter(BaseTest): @@ -140,7 +139,7 @@ def test_fields_and_properties(self): context = HogQLContext( team_id=self.team.pk, within_non_hogql_query=True, - modifiers=HogQLQueryModifiers(personsOnEventsMode=PersonOnEventsMode.DISABLED), + modifiers=HogQLQueryModifiers(personsOnEventsMode=PersonsOnEventsMode.disabled), ) self.assertEqual( self._expr("person.properties.bla", context), @@ -157,7 +156,7 @@ def test_fields_and_properties(self): team_id=self.team.pk, within_non_hogql_query=True, modifiers=HogQLQueryModifiers( - personsOnEventsMode=PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS + personsOnEventsMode=PersonsOnEventsMode.person_id_no_override_properties_on_events ), ) self.assertEqual( diff --git a/posthog/hogql/transforms/property_types.py b/posthog/hogql/transforms/property_types.py index 1f0b34ae3cec3..cc5451bf6bc3a 100644 --- a/posthog/hogql/transforms/property_types.py +++ b/posthog/hogql/transforms/property_types.py @@ -6,7 +6,7 @@ from posthog.hogql.escape_sql import escape_hogql_identifier from posthog.hogql.visitor import CloningVisitor, TraversingVisitor from posthog.models.property import PropertyName, TableColumn -from posthog.utils import PersonOnEventsMode +from posthog.schema import PersonsOnEventsMode def resolve_property_types(node: ast.Expr, context: HogQLContext) -> ast.Expr: @@ -224,10 +224,10 @@ def _add_property_notice( ): property_name = str(node.chain[-1]) if property_type == "person": - if self.context.modifiers.personsOnEventsMode != PersonOnEventsMode.DISABLED: # type: ignore[comparison-overlap] + if self.context.modifiers.personsOnEventsMode != PersonsOnEventsMode.disabled: materialized_column = self._get_materialized_column("events", property_name, "person_properties") else: - materialized_column = self._get_materialized_column("person", property_name, "properties") # type: ignore[unreachable] + materialized_column = self._get_materialized_column("person", property_name, "properties") elif property_type == "group": name_parts = property_name.split("_") name_parts.pop(0) diff --git a/posthog/hogql_queries/actors_query_runner.py b/posthog/hogql_queries/actors_query_runner.py index d745d3384d296..da2e142bf6636 100644 --- a/posthog/hogql_queries/actors_query_runner.py +++ b/posthog/hogql_queries/actors_query_runner.py @@ -105,6 +105,7 @@ def calculate(self) -> ActorsQueryResponse: types=[t for _, t in response.types] if response.types else None, columns=input_columns, hogql=response.hogql, + modifiers=self.modifiers, missing_actors_count=missing_actors_count, **self.paginator.response_params(), ) diff --git a/posthog/hogql_queries/events_query_runner.py b/posthog/hogql_queries/events_query_runner.py index 9bb289b684776..191abd080d2e2 100644 --- a/posthog/hogql_queries/events_query_runner.py +++ b/posthog/hogql_queries/events_query_runner.py @@ -252,6 +252,7 @@ def calculate(self) -> EventsQueryResponse: types=[t for _, t in query_result.types] if query_result.types else None, timings=self.timings.to_list(), hogql=query_result.hogql, + modifiers=self.modifiers, **self.paginator.response_params(), ) diff --git a/posthog/hogql_queries/hogql_query_runner.py b/posthog/hogql_queries/hogql_query_runner.py index 7f30ae435803c..46b4c105a4336 100644 --- a/posthog/hogql_queries/hogql_query_runner.py +++ b/posthog/hogql_queries/hogql_query_runner.py @@ -1,5 +1,5 @@ from datetime import timedelta -from typing import Callable, cast +from typing import Callable, Dict, Optional, cast from posthog.clickhouse.client.connection import Workload from posthog.hogql import ast @@ -26,7 +26,7 @@ class HogQLQueryRunner(QueryRunner): def to_query(self) -> ast.SelectQuery: if self.timings is None: self.timings = HogQLTimings() - values = ( + values: Optional[Dict[str, ast.Expr]] = ( {key: ast.Constant(value=value) for key, value in self.query.values.items()} if self.query.values else None ) with self.timings.measure("parse_select"): 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 35d66a322728e..72dcf1993e1f3 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_correlation_query_runner.py +++ b/posthog/hogql_queries/insights/funnels/funnel_correlation_query_runner.py @@ -207,7 +207,9 @@ def calculate(self) -> FunnelCorrelationResponse: for us to calculate the odds ratio. """ if not self.funnels_query.series: - return FunnelCorrelationResponse(results=FunnelCorrelationResult(events=[], skewed=False)) + return FunnelCorrelationResponse( + results=FunnelCorrelationResult(events=[], skewed=False), modifiers=self.modifiers + ) events, skewed_totals, hogql, response = self._calculate() @@ -223,6 +225,7 @@ def calculate(self) -> FunnelCorrelationResponse: hasMore=response.hasMore, limit=response.limit, offset=response.offset, + modifiers=self.modifiers, ) def _calculate(self) -> tuple[List[EventOddsRatio], bool, str, HogQLQueryResponse]: diff --git a/posthog/hogql_queries/insights/funnels/funnels_query_runner.py b/posthog/hogql_queries/insights/funnels/funnels_query_runner.py index b1ca8dfd6dd54..d2ec04e3e8489 100644 --- a/posthog/hogql_queries/insights/funnels/funnels_query_runner.py +++ b/posthog/hogql_queries/insights/funnels/funnels_query_runner.py @@ -100,7 +100,7 @@ def calculate(self): if response.timings is not None: timings.extend(response.timings) - return FunnelsQueryResponse(results=results, timings=timings, hogql=hogql) + return FunnelsQueryResponse(results=results, timings=timings, hogql=hogql, modifiers=self.modifiers) @cached_property def funnel_order_class(self): diff --git a/posthog/hogql_queries/insights/insight_actors_query_runner.py b/posthog/hogql_queries/insights/insight_actors_query_runner.py index 7d3b2a260845a..ba496af1f6261 100644 --- a/posthog/hogql_queries/insights/insight_actors_query_runner.py +++ b/posthog/hogql_queries/insights/insight_actors_query_runner.py @@ -31,7 +31,7 @@ class InsightActorsQueryRunner(QueryRunner): @cached_property def source_runner(self) -> QueryRunner: - return get_query_runner(self.query.source, self.team, self.timings, self.limit_context) + return get_query_runner(self.query.source, self.team, self.timings, self.limit_context, self.modifiers) def to_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: if isinstance(self.source_runner, TrendsQueryRunner): diff --git a/posthog/hogql_queries/insights/lifecycle_query_runner.py b/posthog/hogql_queries/insights/lifecycle_query_runner.py index ea883eec542bc..42b35d6b4df51 100644 --- a/posthog/hogql_queries/insights/lifecycle_query_runner.py +++ b/posthog/hogql_queries/insights/lifecycle_query_runner.py @@ -212,7 +212,7 @@ def calculate(self) -> LifecycleQueryResponse: } ) - return LifecycleQueryResponse(results=res, timings=response.timings, hogql=hogql) + return LifecycleQueryResponse(results=res, timings=response.timings, hogql=hogql, modifiers=self.modifiers) @cached_property def query_date_range(self): diff --git a/posthog/hogql_queries/insights/paths_query_runner.py b/posthog/hogql_queries/insights/paths_query_runner.py index 34fff8e1d0cc8..ca7890735f814 100644 --- a/posthog/hogql_queries/insights/paths_query_runner.py +++ b/posthog/hogql_queries/insights/paths_query_runner.py @@ -869,7 +869,7 @@ def calculate(self) -> PathsQueryResponse: for source, target, value, avg_conversion_time in response.results ) - return PathsQueryResponse(results=results, timings=response.timings, hogql=hogql) + return PathsQueryResponse(results=results, timings=response.timings, hogql=hogql, modifiers=self.modifiers) @property def extra_event_fields_and_properties(self) -> list[str]: diff --git a/posthog/hogql_queries/insights/retention_query_runner.py b/posthog/hogql_queries/insights/retention_query_runner.py index 3ac2c5b4b5462..ac15ded6728b1 100644 --- a/posthog/hogql_queries/insights/retention_query_runner.py +++ b/posthog/hogql_queries/insights/retention_query_runner.py @@ -340,7 +340,7 @@ def calculate(self) -> RetentionQueryResponse: for first_interval in range(self.query_date_range.total_intervals) ] - return RetentionQueryResponse(results=results, timings=response.timings, hogql=hogql) + return RetentionQueryResponse(results=results, timings=response.timings, hogql=hogql, modifiers=self.modifiers) def to_actors_query(self, interval: Optional[int] = None) -> ast.SelectQuery: with self.timings.measure("retention_query"): diff --git a/posthog/hogql_queries/insights/stickiness_query_runner.py b/posthog/hogql_queries/insights/stickiness_query_runner.py index 184e3c0af02df..d9096f05853b6 100644 --- a/posthog/hogql_queries/insights/stickiness_query_runner.py +++ b/posthog/hogql_queries/insights/stickiness_query_runner.py @@ -248,7 +248,7 @@ def calculate(self): res.append(series_object) - return StickinessQueryResponse(results=res, timings=timings) + return StickinessQueryResponse(results=res, timings=timings, modifiers=self.modifiers) def where_clause(self, series_with_extra: SeriesWithExtras) -> ast.Expr: date_range = self.date_range(series_with_extra) diff --git a/posthog/hogql_queries/insights/trends/aggregation_operations.py b/posthog/hogql_queries/insights/trends/aggregation_operations.py index a0307d625c9a1..1c356277548d0 100644 --- a/posthog/hogql_queries/insights/trends/aggregation_operations.py +++ b/posthog/hogql_queries/insights/trends/aggregation_operations.py @@ -1,9 +1,10 @@ from typing import List, Optional, cast, Union +from posthog.constants import NON_TIME_SERIES_DISPLAY_TYPES from posthog.hogql import ast from posthog.hogql.parser import parse_expr, parse_select from posthog.hogql_queries.utils.query_date_range import QueryDateRange from posthog.models.team.team import Team -from posthog.schema import EventsNode, ActionsNode, DataWarehouseNode +from posthog.schema import BaseMathType, ChartDisplayType, EventsNode, ActionsNode, DataWarehouseNode from posthog.models.filters.mixins.utils import cached_property from posthog.hogql_queries.insights.data_warehouse_mixin import DataWarehouseInsightQueryMixin @@ -52,6 +53,7 @@ def replace_select_from(self, join_expr: ast.JoinExpr) -> None: class AggregationOperations(DataWarehouseInsightQueryMixin): team: Team series: Union[EventsNode, ActionsNode, DataWarehouseNode] + chart_display_type: ChartDisplayType query_date_range: QueryDateRange should_aggregate_values: bool @@ -59,11 +61,13 @@ def __init__( self, team: Team, series: Union[EventsNode, ActionsNode, DataWarehouseNode], + chart_display_type: ChartDisplayType, query_date_range: QueryDateRange, should_aggregate_values: bool, ) -> None: self.team = team self.series = series + self.chart_display_type = chart_display_type self.query_date_range = query_date_range self.should_aggregate_values = should_aggregate_values @@ -317,9 +321,20 @@ def _inner_select_query( def _events_query( self, events_where_clause: ast.Expr, sample_value: ast.RatioExpr ) -> ast.SelectQuery | ast.SelectUnionQuery: + date_from_with_lookback = "{date_from} - {inclusive_lookback}" + if self.chart_display_type in NON_TIME_SERIES_DISPLAY_TYPES and self.series.math in ( + BaseMathType.weekly_active, + BaseMathType.monthly_active, + ): + # TRICKY: On total value (non-time-series) insights, WAU/MAU math is simply meaningless. + # There's no intuitive way to define the semantics of such a combination, so what we do is just turn it + # into a count of unique users between `date_to - INTERVAL (7|30) DAY` and `date_to`. + # This way we at least ensure the date range is the probably expected 7 or 30 days. + date_from_with_lookback = "{date_to} - {inclusive_lookback}" + date_filters = [ parse_expr( - "timestamp >= {date_from} - {inclusive_lookback}", + f"timestamp >= {date_from_with_lookback}", placeholders={ **self.query_date_range.to_placeholders(), **self._interval_placeholders(), diff --git a/posthog/hogql_queries/insights/trends/breakdown_values.py b/posthog/hogql_queries/insights/trends/breakdown_values.py index 3646bb7e3e565..fb349f279d19a 100644 --- a/posthog/hogql_queries/insights/trends/breakdown_values.py +++ b/posthog/hogql_queries/insights/trends/breakdown_values.py @@ -270,6 +270,7 @@ def _aggregation_operation(self) -> AggregationOperations: return AggregationOperations( self.team, self.series, + self.chart_display_type, self.query_date_range, should_aggregate_values=True, # doesn't matter in this case ) 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 a5a810cb33c2a..5885c57710928 100644 --- a/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr +++ b/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr @@ -503,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'), true), 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'), true), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-11 23:59:59', 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)) @@ -561,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'), true), 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'), true), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-11 23:59:59', 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)) @@ -4662,7 +4662,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), equals(e.event, '$pageview'), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-11 00:00:00', 6, 'UTC')), toIntervalDay(7))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 23:59:59', 6, 'UTC'))), 0)) + WHERE and(equals(e.team_id, 2), equals(e.event, '$pageview'), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 23:59:59', 6, 'UTC')), toIntervalDay(7))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 23:59:59', 6, 'UTC'))), 0)) GROUP BY timestamp, actor_id) AS e WHERE and(ifNull(lessOrEquals(e.timestamp, plus(d.timestamp, toIntervalDay(1))), 0), ifNull(greater(e.timestamp, minus(d.timestamp, toIntervalDay(6))), 0)) GROUP BY d.timestamp, @@ -4681,8 +4681,8 @@ (SELECT d.timestamp AS timestamp, e.actor_id AS actor_id FROM - (SELECT minus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-08 23:59:59', 6, 'UTC'))), toIntervalDay(numbers.number)) AS timestamp - FROM numbers(dateDiff('day', minus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), toIntervalDay(7)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-08 23:59:59', 6, 'UTC')))) AS numbers) AS d + (SELECT minus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-18 23:59:59', 6, 'UTC'))), toIntervalDay(numbers.number)) AS timestamp + FROM numbers(dateDiff('day', minus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), toIntervalDay(7)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-18 23:59:59', 6, 'UTC')))) AS numbers) AS d CROSS JOIN (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp, e__pdi.person_id AS actor_id @@ -4694,13 +4694,13 @@ 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), equals(e.event, '$pageview'), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')), toIntervalDay(7))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-08 23:59:59', 6, 'UTC'))), 0)) + WHERE and(equals(e.team_id, 2), equals(e.event, '$pageview'), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-18 23:59:59', 6, 'UTC')), toIntervalDay(7))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-18 23:59:59', 6, 'UTC'))), 0)) GROUP BY timestamp, actor_id) AS e WHERE and(ifNull(lessOrEquals(e.timestamp, plus(d.timestamp, toIntervalDay(1))), 0), ifNull(greater(e.timestamp, minus(d.timestamp, toIntervalDay(6))), 0)) GROUP BY d.timestamp, e.actor_id ORDER BY d.timestamp ASC) - WHERE and(ifNull(greaterOrEquals(timestamp, toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-08 23:59:59', 6, 'UTC'))), 0)) + WHERE and(ifNull(greaterOrEquals(timestamp, toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-18 23:59:59', 6, 'UTC'))), 0)) LIMIT 10000 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 @@ -4713,8 +4713,8 @@ (SELECT d.timestamp AS timestamp, e.actor_id AS actor_id FROM - (SELECT minus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-08 23:59:59', 6, 'UTC'))), toIntervalDay(numbers.number)) AS timestamp - FROM numbers(dateDiff('day', minus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), toIntervalDay(7)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-08 23:59:59', 6, 'UTC')))) AS numbers) AS d + (SELECT minus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-18 23:59:59', 6, 'UTC'))), toIntervalDay(numbers.number)) AS timestamp + FROM numbers(dateDiff('day', minus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), toIntervalDay(7)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-18 23:59:59', 6, 'UTC')))) AS numbers) AS d CROSS JOIN (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp, e__pdi.person_id AS actor_id @@ -4726,13 +4726,13 @@ 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), equals(e.event, '$pageview'), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')), toIntervalDay(7))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-08 23:59:59', 6, 'UTC'))), 0)) + WHERE and(equals(e.team_id, 2), equals(e.event, '$pageview'), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-18 23:59:59', 6, 'UTC')), toIntervalDay(7))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-18 23:59:59', 6, 'UTC'))), 0)) GROUP BY timestamp, actor_id) AS e WHERE and(ifNull(lessOrEquals(e.timestamp, plus(d.timestamp, toIntervalDay(1))), 0), ifNull(greater(e.timestamp, minus(d.timestamp, toIntervalDay(6))), 0)) GROUP BY d.timestamp, e.actor_id ORDER BY d.timestamp ASC) - WHERE and(ifNull(greaterOrEquals(timestamp, toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-08 23:59:59', 6, 'UTC'))), 0)) + WHERE and(ifNull(greaterOrEquals(timestamp, toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-18 23:59:59', 6, 'UTC'))), 0)) LIMIT 10000 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 diff --git a/posthog/hogql_queries/insights/trends/test/test_aggregation_operations.py b/posthog/hogql_queries/insights/trends/test/test_aggregation_operations.py index e69eb3e96f8b7..6dfb40247f364 100644 --- a/posthog/hogql_queries/insights/trends/test/test_aggregation_operations.py +++ b/posthog/hogql_queries/insights/trends/test/test_aggregation_operations.py @@ -12,6 +12,7 @@ from posthog.models.team.team import Team from posthog.schema import ( BaseMathType, + ChartDisplayType, CountPerActorMathType, EventsNode, PropertyMathType, @@ -101,7 +102,7 @@ def test_all_cases_return( series = EventsNode(event="$pageview", math=math, math_property=math_property) query_date_range = QueryDateRange(date_range=None, interval=None, now=datetime.now(), team=team) - agg_ops = AggregationOperations(team, series, query_date_range, False) + agg_ops = AggregationOperations(team, series, ChartDisplayType.ActionsLineGraph, query_date_range, False) res = agg_ops.select_aggregation() assert isinstance(res, ast.Expr) @@ -146,6 +147,6 @@ def test_requiring_query_orchestration( series = EventsNode(event="$pageview", math=math) query_date_range = QueryDateRange(date_range=None, interval=None, now=datetime.now(), team=team) - agg_ops = AggregationOperations(team, series, query_date_range, False) + agg_ops = AggregationOperations(team, series, ChartDisplayType.ActionsLineGraph, query_date_range, False) res = agg_ops.requires_query_orchestration() assert res == result diff --git a/posthog/hogql_queries/insights/trends/test/test_trends.py b/posthog/hogql_queries/insights/trends/test/test_trends.py index de90500783ffd..8ba4aea1b3459 100644 --- a/posthog/hogql_queries/insights/trends/test/test_trends.py +++ b/posthog/hogql_queries/insights/trends/test/test_trends.py @@ -5948,7 +5948,7 @@ def test_weekly_active_users_aggregated_range_wider_than_week(self): data = { "date_from": "2020-01-01", - "date_to": "2020-01-08", + "date_to": "2020-01-18", "display": TRENDS_TABLE, "events": [ { @@ -5962,7 +5962,7 @@ def test_weekly_active_users_aggregated_range_wider_than_week(self): filter = Filter(team=self.team, data=data) result = self._run(filter, self.team) - # Only p0 was active on 2020-01-08 or in the preceding 6 days + # Only p0 was active on 2020-01-18 or in the preceding 6 days self.assertEqual(result[0]["aggregated_value"], 1) @snapshot_clickhouse_queries @@ -5972,7 +5972,7 @@ def test_weekly_active_users_aggregated_range_wider_than_week_with_sampling(self data = { "sampling_factor": 1, "date_from": "2020-01-01", - "date_to": "2020-01-08", + "date_to": "2020-01-18", "display": TRENDS_TABLE, "events": [ { @@ -5986,7 +5986,7 @@ def test_weekly_active_users_aggregated_range_wider_than_week_with_sampling(self filter = Filter(team=self.team, data=data) result = self._run(filter, self.team) - # Only p0 was active on 2020-01-08 or in the preceding 6 days + # Only p0 was active on 2020-01-18 or in the preceding 6 days self.assertEqual(result[0]["aggregated_value"], 1) @snapshot_clickhouse_queries diff --git a/posthog/hogql_queries/insights/trends/trends_query_builder.py b/posthog/hogql_queries/insights/trends/trends_query_builder.py index 31d4c93cc90d8..82fbb849ef5d9 100644 --- a/posthog/hogql_queries/insights/trends/trends_query_builder.py +++ b/posthog/hogql_queries/insights/trends/trends_query_builder.py @@ -502,16 +502,13 @@ def _events_filter( ] ) elif not self._aggregation_operation.requires_query_orchestration(): + date_range_placeholders = self.query_date_range.to_placeholders() filters.extend( [ parse_expr( - "timestamp >= {date_from_with_adjusted_start_of_interval}", - placeholders=self.query_date_range.to_placeholders(), - ), - parse_expr( - "timestamp <= {date_to}", - placeholders=self.query_date_range.to_placeholders(), + "timestamp >= {date_from_with_adjusted_start_of_interval}", placeholders=date_range_placeholders ), + parse_expr("timestamp <= {date_to}", placeholders=date_range_placeholders), ] ) @@ -617,7 +614,11 @@ def _breakdown(self, is_actors_query: bool, breakdown_values_override: Optional[ @cached_property def _aggregation_operation(self) -> AggregationOperations: return AggregationOperations( - self.team, self.series, self.query_date_range, self._trends_display.should_aggregate_values() + self.team, + self.series, + self._trends_display.display_type, + self.query_date_range, + self._trends_display.should_aggregate_values(), ) @cached_property diff --git a/posthog/hogql_queries/insights/trends/trends_query_runner.py b/posthog/hogql_queries/insights/trends/trends_query_runner.py index 59bc941dd4fb0..d3cf45e5055be 100644 --- a/posthog/hogql_queries/insights/trends/trends_query_runner.py +++ b/posthog/hogql_queries/insights/trends/trends_query_runner.py @@ -358,7 +358,7 @@ def run(index: int, query: ast.SelectQuery | ast.SelectUnionQuery, is_parallel: with self.timings.measure("apply_formula"): res = self.apply_formula(self.query.trendsFilter.formula, res) - return TrendsQueryResponse(results=res, timings=timings, hogql=response_hogql) + return TrendsQueryResponse(results=res, timings=timings, hogql=response_hogql, modifiers=self.modifiers) def build_series_response(self, response: HogQLQueryResponse, series: SeriesWithExtras, series_count: int): if response.results is None: diff --git a/posthog/hogql_queries/query_runner.py b/posthog/hogql_queries/query_runner.py index 3cd3d1e68bf9a..25a903e839335 100644 --- a/posthog/hogql_queries/query_runner.py +++ b/posthog/hogql_queries/query_runner.py @@ -70,6 +70,7 @@ class QueryResponse(BaseModel, Generic[DataT]): limit: Optional[int] = None offset: Optional[int] = None samplingRate: Optional[SamplingRate] = None + modifiers: Optional[HogQLQueryModifiers] = None class CachedQueryResponse(QueryResponse): @@ -284,7 +285,8 @@ def __init__( self.team = team self.timings = timings or HogQLTimings() self.limit_context = limit_context or LimitContext.QUERY - self.modifiers = create_default_modifiers_for_team(team, modifiers) + _modifiers = modifiers or (query.modifiers if hasattr(query, "modifiers") else None) + self.modifiers = create_default_modifiers_for_team(team, _modifiers) if not self.is_query_node(query): query = self.query_type.model_validate(query) diff --git a/posthog/hogql_queries/web_analytics/stats_table.py b/posthog/hogql_queries/web_analytics/stats_table.py index f15505365ba66..4d1a2d7be5da5 100644 --- a/posthog/hogql_queries/web_analytics/stats_table.py +++ b/posthog/hogql_queries/web_analytics/stats_table.py @@ -185,6 +185,7 @@ def calculate(self): timings=response.timings, types=response.types, hogql=response.hogql, + modifiers=self.modifiers, **self.paginator.response_params(), ) diff --git a/posthog/hogql_queries/web_analytics/top_clicks.py b/posthog/hogql_queries/web_analytics/top_clicks.py index 192d7b279b704..2258302eecc7f 100644 --- a/posthog/hogql_queries/web_analytics/top_clicks.py +++ b/posthog/hogql_queries/web_analytics/top_clicks.py @@ -59,6 +59,7 @@ def calculate(self): results=response.results, timings=response.timings, types=response.types, + modifiers=self.modifiers, ) @cached_property diff --git a/posthog/hogql_queries/web_analytics/web_overview.py b/posthog/hogql_queries/web_analytics/web_overview.py index 9710a2ffa3dde..8d0a6544b5fa4 100644 --- a/posthog/hogql_queries/web_analytics/web_overview.py +++ b/posthog/hogql_queries/web_analytics/web_overview.py @@ -308,6 +308,7 @@ def calculate(self): to_data("bounce rate", "percentage", row[8], row[9], is_increase_bad=True), ], samplingRate=self._sample_rate, + modifiers=self.modifiers, ) @cached_property diff --git a/posthog/models/subscription.py b/posthog/models/subscription.py index 3680155f7df27..f7b8a90a7e492 100644 --- a/posthog/models/subscription.py +++ b/posthog/models/subscription.py @@ -124,12 +124,16 @@ def rrule(self): ) def set_next_delivery_date(self, from_dt=None): - self.next_delivery_date = self.rrule.after(dt=from_dt or timezone.now(), inc=False) + # We never want next_delivery_date to be in the past + now = timezone.now() + timedelta(minutes=15) # Buffer of 15 minutes since we might run a bit early + self.next_delivery_date = self.rrule.after(dt=max(from_dt or now, now), inc=False) def save(self, *args, **kwargs) -> None: # Only if the schedule has changed do we update the next delivery date if not self.id or str(self._rrule) != str(self.rrule): self.set_next_delivery_date() + if "update_fields" in kwargs: + kwargs["update_fields"].append("next_delivery_date") super(Subscription, self).save(*args, **kwargs) @property diff --git a/posthog/models/team/team.py b/posthog/models/team/team.py index c199c6bcc1d56..19cb99cf67762 100644 --- a/posthog/models/team/team.py +++ b/posthog/models/team/team.py @@ -33,10 +33,10 @@ sane_repr, ) from posthog.settings.utils import get_list -from posthog.utils import GenericEmails, PersonOnEventsMode +from posthog.utils import GenericEmails from .team_caching import get_team_in_cache, set_team_in_cache -from ...schema import PathCleaningFilter +from ...schema import PathCleaningFilter, PersonsOnEventsMode if TYPE_CHECKING: from posthog.models.user import User @@ -289,33 +289,33 @@ def aggregate_users_by_distinct_id(self) -> bool: objects: TeamManager = TeamManager() @property - def person_on_events_mode(self) -> PersonOnEventsMode: + def person_on_events_mode(self) -> PersonsOnEventsMode: if self._person_on_events_person_id_override_properties_on_events: - tag_queries(person_on_events_mode=PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS) - return PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS + tag_queries(person_on_events_mode=PersonsOnEventsMode.person_id_override_properties_on_events) + return PersonsOnEventsMode.person_id_override_properties_on_events if self._person_on_events_person_id_no_override_properties_on_events: # also tag person_on_events_enabled for legacy compatibility tag_queries( person_on_events_enabled=True, - person_on_events_mode=PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS, + person_on_events_mode=PersonsOnEventsMode.person_id_no_override_properties_on_events, ) - return PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS + return PersonsOnEventsMode.person_id_no_override_properties_on_events if self._person_on_events_person_id_override_properties_joined: tag_queries( person_on_events_enabled=True, - person_on_events_mode=PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_JOINED, + person_on_events_mode=PersonsOnEventsMode.person_id_override_properties_joined, ) - return PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_JOINED + return PersonsOnEventsMode.person_id_override_properties_joined - return PersonOnEventsMode.DISABLED + return PersonsOnEventsMode.disabled # KLUDGE: DO NOT REFERENCE IN THE BACKEND! # Keeping this property for now only to be used by the frontend in certain cases @property def person_on_events_querying_enabled(self) -> bool: - return self.person_on_events_mode != PersonOnEventsMode.DISABLED + return self.person_on_events_mode != PersonsOnEventsMode.disabled @property def _person_on_events_person_id_no_override_properties_on_events(self) -> bool: diff --git a/posthog/models/test/test_subscription_model.py b/posthog/models/test/test_subscription_model.py index 0369c7d3c590e..e63b69856067e 100644 --- a/posthog/models/test/test_subscription_model.py +++ b/posthog/models/test/test_subscription_model.py @@ -71,6 +71,40 @@ def test_only_updates_next_delivery_date_if_rrule_changes(self): subscription.save() assert old_date == subscription.next_delivery_date + @freeze_time("2022-01-11 09:55:00") + def test_set_next_delivery_date_when_in_upcoming_delta(self): + subscription = Subscription.objects.create( + id=1, + team=self.team, + title="Daily Subscription", + target_type="email", + target_value="tests@posthog.com", + frequency="daily", + start_date=datetime(2022, 1, 1, 10, 0, 0, 0).replace(tzinfo=ZoneInfo("UTC")), + next_delivery_date=datetime(2022, 1, 11, 10, 0, 0, 0).replace(tzinfo=ZoneInfo("UTC")), + ) + + subscription.set_next_delivery_date(subscription.next_delivery_date) + + assert subscription.next_delivery_date == datetime(2022, 1, 12, 10, 0, 0, 0).replace(tzinfo=ZoneInfo("UTC")) + + @freeze_time("2022-01-11 09:55:00") + def test_set_next_delivery_date_when_days_behind(self): + subscription = Subscription.objects.create( + id=1, + team=self.team, + title="Daily Subscription", + target_type="email", + target_value="tests@posthog.com", + frequency="daily", + start_date=datetime(2022, 1, 1, 10, 0, 0, 0).replace(tzinfo=ZoneInfo("UTC")), + next_delivery_date=datetime(2022, 1, 2, 10, 0, 0, 0).replace(tzinfo=ZoneInfo("UTC")), + ) + + subscription.set_next_delivery_date(subscription.next_delivery_date) + + assert subscription.next_delivery_date == datetime(2022, 1, 12, 10, 0, 0, 0).replace(tzinfo=ZoneInfo("UTC")) + def test_generating_token(self): subscription = self._create_insight_subscription( target_value="test1@posthog.com,test2@posthog.com,test3@posthog.com" diff --git a/posthog/queries/breakdown_props.py b/posthog/queries/breakdown_props.py index 64a550edf543d..397ee061332e6 100644 --- a/posthog/queries/breakdown_props.py +++ b/posthog/queries/breakdown_props.py @@ -30,13 +30,13 @@ from posthog.queries.person_on_events_v2_sql import PERSON_OVERRIDES_JOIN_SQL from posthog.queries.person_query import PersonQuery from posthog.queries.query_date_range import QueryDateRange +from posthog.schema import PersonsOnEventsMode from posthog.session_recordings.queries.session_query import SessionQuery from posthog.queries.trends.sql import ( HISTOGRAM_ELEMENTS_ARRAY_OF_KEY_SQL, TOP_ELEMENTS_ARRAY_OF_KEY_SQL, ) from posthog.queries.util import PersonPropertiesMode -from posthog.utils import PersonOnEventsMode ALL_USERS_COHORT_ID = 0 @@ -84,7 +84,7 @@ def get_breakdown_prop_values( sessions_join_params: Dict = {} null_person_filter = ( - f"AND notEmpty(e.person_id)" if team.person_on_events_mode != PersonOnEventsMode.DISABLED else "" + f"AND notEmpty(e.person_id)" if team.person_on_events_mode != PersonsOnEventsMode.disabled else "" ) if person_properties_mode == PersonPropertiesMode.DIRECT_ON_EVENTS: diff --git a/posthog/queries/event_query/event_query.py b/posthog/queries/event_query/event_query.py index aa292e7ee44e4..bcd7002e66f47 100644 --- a/posthog/queries/event_query/event_query.py +++ b/posthog/queries/event_query/event_query.py @@ -18,9 +18,9 @@ from posthog.queries.person_distinct_id_query import get_team_distinct_ids_query from posthog.queries.person_query import PersonQuery from posthog.queries.query_date_range import QueryDateRange +from posthog.schema import PersonsOnEventsMode from posthog.session_recordings.queries.session_query import SessionQuery from posthog.queries.util import PersonPropertiesMode -from posthog.utils import PersonOnEventsMode from posthog.queries.person_on_events_v2_sql import PERSON_OVERRIDES_JOIN_SQL @@ -64,7 +64,7 @@ def __init__( extra_event_properties: List[PropertyName] = [], extra_person_fields: List[ColumnName] = [], override_aggregate_users_by_distinct_id: Optional[bool] = None, - person_on_events_mode: PersonOnEventsMode = PersonOnEventsMode.DISABLED, + person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.disabled, **kwargs, ) -> None: self._filter = filter @@ -120,9 +120,9 @@ def _determine_should_join_distinct_ids(self) -> None: pass def _get_person_id_alias(self, person_on_events_mode) -> str: - if person_on_events_mode == PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: + if person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: return f"if(notEmpty({self.PERSON_ID_OVERRIDES_TABLE_ALIAS}.person_id), {self.PERSON_ID_OVERRIDES_TABLE_ALIAS}.person_id, {self.EVENT_TABLE_ALIAS}.person_id)" - elif person_on_events_mode == PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: + elif person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: return f"{self.EVENT_TABLE_ALIAS}.person_id" return f"{self.DISTINCT_ID_TABLE_ALIAS}.person_id" @@ -131,7 +131,7 @@ def _get_person_ids_query(self, *, relevant_events_conditions: str = "") -> str: if not self._should_join_distinct_ids: return "" - if self._person_on_events_mode == PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: + if self._person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: return PERSON_OVERRIDES_JOIN_SQL.format( person_overrides_table_alias=self.PERSON_ID_OVERRIDES_TABLE_ALIAS, event_table_alias=self.EVENT_TABLE_ALIAS, diff --git a/posthog/queries/foss_cohort_query.py b/posthog/queries/foss_cohort_query.py index 03ddf348e181a..91d16ec3ec5a4 100644 --- a/posthog/queries/foss_cohort_query.py +++ b/posthog/queries/foss_cohort_query.py @@ -23,7 +23,8 @@ from posthog.models.property.util import prop_filter_json_extract, parse_prop_grouped_clauses from posthog.queries.event_query import EventQuery from posthog.queries.util import PersonPropertiesMode -from posthog.utils import PersonOnEventsMode, relative_date_parse +from posthog.schema import PersonsOnEventsMode +from posthog.utils import relative_date_parse Relative_Date = Tuple[int, OperatorInterval] Event = Tuple[str, Union[str, int]] @@ -299,7 +300,7 @@ def _build_sources(self, subq: List[Tuple[str, str]]) -> Tuple[str, str]: fields = f"{subq_alias}.person_id" elif prev_alias: # can't join without a previous alias if subq_alias == self.PERSON_TABLE_ALIAS and self.should_pushdown_persons: - if self._person_on_events_mode == PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: + if self._person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: # when using person-on-events, instead of inner join, we filter inside # the event query itself continue @@ -330,11 +331,11 @@ def _get_behavior_subquery(self) -> Tuple[str, Dict[str, Any], str]: query, params = "", {} if self._should_join_behavioral_query: _fields = [ - f"{self.DISTINCT_ID_TABLE_ALIAS if self._person_on_events_mode == PersonOnEventsMode.DISABLED else self.EVENT_TABLE_ALIAS}.person_id AS person_id" + f"{self.DISTINCT_ID_TABLE_ALIAS if self._person_on_events_mode == PersonsOnEventsMode.disabled else self.EVENT_TABLE_ALIAS}.person_id AS person_id" ] _fields.extend(self._fields) - if self.should_pushdown_persons and self._person_on_events_mode != PersonOnEventsMode.DISABLED: + if self.should_pushdown_persons and self._person_on_events_mode != PersonsOnEventsMode.disabled: person_prop_query, person_prop_params = self._get_prop_groups( self._inner_property_groups, person_properties_mode=PersonPropertiesMode.DIRECT_ON_EVENTS, @@ -550,7 +551,7 @@ def get_performed_event_multiple(self, prop: Property, prepend: str, idx: int) - def _determine_should_join_distinct_ids(self) -> None: self._should_join_distinct_ids = ( - self._person_on_events_mode != PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS + self._person_on_events_mode != PersonsOnEventsMode.person_id_no_override_properties_on_events ) def _determine_should_join_persons(self) -> None: diff --git a/posthog/queries/funnels/base.py b/posthog/queries/funnels/base.py index 244ede082b7ac..a96ba9b9f7f7c 100644 --- a/posthog/queries/funnels/base.py +++ b/posthog/queries/funnels/base.py @@ -33,7 +33,8 @@ from posthog.queries.funnels.funnel_event_query import FunnelEventQuery from posthog.queries.insight import insight_sync_execute from posthog.queries.util import correct_result_for_sampling, get_person_properties_mode -from posthog.utils import PersonOnEventsMode, relative_date_parse, generate_short_id +from posthog.schema import PersonsOnEventsMode +from posthog.utils import relative_date_parse, generate_short_id class ClickhouseFunnelBase(ABC): @@ -727,7 +728,7 @@ def _get_breakdown_select_prop(self) -> Tuple[str, Dict[str, Any]]: self.params.update({"breakdown": self._filter.breakdown}) if self._filter.breakdown_type == "person": - if self._team.person_on_events_mode != PersonOnEventsMode.DISABLED: + if self._team.person_on_events_mode != PersonsOnEventsMode.disabled: basic_prop_selector, basic_prop_params = get_single_or_multi_property_string_expr( self._filter.breakdown, table="events", @@ -757,7 +758,7 @@ def _get_breakdown_select_prop(self) -> Tuple[str, Dict[str, Any]]: # :TRICKY: We only support string breakdown for group properties assert isinstance(self._filter.breakdown, str) - if self._team.person_on_events_mode != PersonOnEventsMode.DISABLED and groups_on_events_querying_enabled(): + if self._team.person_on_events_mode != PersonsOnEventsMode.disabled and groups_on_events_querying_enabled(): properties_field = f"group{self._filter.breakdown_group_type_index}_properties" expression, _ = get_property_string_expr( table="events", diff --git a/posthog/queries/funnels/funnel_event_query.py b/posthog/queries/funnels/funnel_event_query.py index d2393f77b5d21..2c8ad72524f70 100644 --- a/posthog/queries/funnels/funnel_event_query.py +++ b/posthog/queries/funnels/funnel_event_query.py @@ -6,7 +6,7 @@ from posthog.models.group.util import get_aggregation_target_field from posthog.queries.event_query import EventQuery from posthog.queries.util import get_person_properties_mode -from posthog.utils import PersonOnEventsMode +from posthog.schema import PersonsOnEventsMode class FunnelEventQuery(EventQuery): @@ -49,7 +49,7 @@ def get_query( _fields += [f"{self.EVENT_TABLE_ALIAS}.{field} AS {field}" for field in self._extra_fields] - if self._person_on_events_mode != PersonOnEventsMode.DISABLED: + if self._person_on_events_mode != PersonsOnEventsMode.disabled: _fields += [f"{self._person_id_alias} as person_id"] _fields.extend( @@ -95,7 +95,7 @@ def get_query( null_person_filter = ( f"AND notEmpty({self.EVENT_TABLE_ALIAS}.person_id)" - if self._person_on_events_mode != PersonOnEventsMode.DISABLED + if self._person_on_events_mode != PersonsOnEventsMode.disabled else "" ) @@ -131,9 +131,9 @@ def _determine_should_join_distinct_ids(self) -> None: ) is_using_cohort_propertes = self._column_optimizer.is_using_cohort_propertes - if self._person_on_events_mode == PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: + if self._person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: self._should_join_distinct_ids = True - elif self._person_on_events_mode == PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS or ( + elif self._person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events or ( non_person_id_aggregation and not is_using_cohort_propertes ): self._should_join_distinct_ids = False @@ -142,7 +142,7 @@ def _determine_should_join_distinct_ids(self) -> None: def _determine_should_join_persons(self) -> None: EventQuery._determine_should_join_persons(self) - if self._person_on_events_mode != PersonOnEventsMode.DISABLED: + if self._person_on_events_mode != PersonsOnEventsMode.disabled: self._should_join_persons = False def _get_entity_query(self, entities=None, entity_name="events") -> Tuple[str, Dict[str, Any]]: diff --git a/posthog/queries/groups_join_query/groups_join_query.py b/posthog/queries/groups_join_query/groups_join_query.py index fb8b46ae591b9..2cc62849cacc3 100644 --- a/posthog/queries/groups_join_query/groups_join_query.py +++ b/posthog/queries/groups_join_query/groups_join_query.py @@ -5,7 +5,7 @@ from posthog.models.filters.retention_filter import RetentionFilter from posthog.models.filters.stickiness_filter import StickinessFilter from posthog.queries.column_optimizer.column_optimizer import ColumnOptimizer -from posthog.utils import PersonOnEventsMode +from posthog.schema import PersonsOnEventsMode class GroupsJoinQuery: @@ -23,7 +23,7 @@ def __init__( team_id: int, column_optimizer: Optional[ColumnOptimizer] = None, join_key: Optional[str] = None, - person_on_events_mode: PersonOnEventsMode = PersonOnEventsMode.DISABLED, + person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.disabled, ) -> None: self._filter = filter self._team_id = team_id diff --git a/posthog/queries/paths/paths_event_query.py b/posthog/queries/paths/paths_event_query.py index 5c222558973a2..61b032aa663ec 100644 --- a/posthog/queries/paths/paths_event_query.py +++ b/posthog/queries/paths/paths_event_query.py @@ -14,7 +14,7 @@ from posthog.models.team import Team from posthog.queries.event_query import EventQuery from posthog.queries.util import get_person_properties_mode -from posthog.utils import PersonOnEventsMode +from posthog.schema import PersonsOnEventsMode class PathEventQuery(EventQuery): @@ -116,7 +116,7 @@ def get_query(self) -> Tuple[str, Dict[str, Any]]: null_person_filter = ( f"AND notEmpty({self.EVENT_TABLE_ALIAS}.person_id)" - if self._person_on_events_mode != PersonOnEventsMode.DISABLED + if self._person_on_events_mode != PersonsOnEventsMode.disabled else "" ) @@ -141,14 +141,14 @@ def get_query(self) -> Tuple[str, Dict[str, Any]]: return query, self.params def _determine_should_join_distinct_ids(self) -> None: - if self._person_on_events_mode == PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: + if self._person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: self._should_join_distinct_ids = False else: self._should_join_distinct_ids = True def _determine_should_join_persons(self) -> None: EventQuery._determine_should_join_persons(self) - if self._person_on_events_mode != PersonOnEventsMode.DISABLED: + if self._person_on_events_mode != PersonsOnEventsMode.disabled: self._should_join_persons = False def _get_grouping_fields(self) -> Tuple[List[str], Dict[str, Any]]: diff --git a/posthog/queries/retention/retention.py b/posthog/queries/retention/retention.py index 5536a02940b9e..8f8b0d89254bf 100644 --- a/posthog/queries/retention/retention.py +++ b/posthog/queries/retention/retention.py @@ -14,7 +14,7 @@ from posthog.queries.retention.sql import RETENTION_BREAKDOWN_SQL from posthog.queries.retention.types import BreakdownValues, CohortKey from posthog.queries.util import correct_result_for_sampling -from posthog.utils import PersonOnEventsMode +from posthog.schema import PersonsOnEventsMode class Retention: @@ -166,7 +166,7 @@ def build_returning_event_query( filter: RetentionFilter, team: Team, aggregate_users_by_distinct_id: Optional[bool] = None, - person_on_events_mode: PersonOnEventsMode = PersonOnEventsMode.DISABLED, + person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.disabled, retention_events_query=RetentionEventsQuery, ) -> Tuple[str, Dict[str, Any]]: returning_event_query_templated, returning_event_params = retention_events_query( @@ -184,7 +184,7 @@ def build_target_event_query( filter: RetentionFilter, team: Team, aggregate_users_by_distinct_id: Optional[bool] = None, - person_on_events_mode: PersonOnEventsMode = PersonOnEventsMode.DISABLED, + person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.disabled, retention_events_query=RetentionEventsQuery, ) -> Tuple[str, Dict[str, Any]]: target_event_query_templated, target_event_params = retention_events_query( diff --git a/posthog/queries/retention/retention_events_query.py b/posthog/queries/retention/retention_events_query.py index 871d9dbb3e033..e84e4bc1e91cc 100644 --- a/posthog/queries/retention/retention_events_query.py +++ b/posthog/queries/retention/retention_events_query.py @@ -14,7 +14,7 @@ from posthog.models.team import Team from posthog.queries.event_query import EventQuery from posthog.queries.util import get_person_properties_mode, get_start_of_interval_sql -from posthog.utils import PersonOnEventsMode +from posthog.schema import PersonsOnEventsMode class RetentionEventsQuery(EventQuery): @@ -27,7 +27,7 @@ def __init__( event_query_type: RetentionQueryType, team: Team, aggregate_users_by_distinct_id: Optional[bool] = None, - person_on_events_mode: PersonOnEventsMode = PersonOnEventsMode.DISABLED, + person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.disabled, ): self._event_query_type = event_query_type super().__init__( @@ -56,14 +56,14 @@ def get_query(self) -> Tuple[str, Dict[str, Any]]: materalised_table_column = "properties" if breakdown_type == "person": - table = "person" if self._person_on_events_mode == PersonOnEventsMode.DISABLED else "events" + table = "person" if self._person_on_events_mode == PersonsOnEventsMode.disabled else "events" column = ( "person_props" - if self._person_on_events_mode == PersonOnEventsMode.DISABLED + if self._person_on_events_mode == PersonsOnEventsMode.disabled else "person_properties" ) materalised_table_column = ( - "properties" if self._person_on_events_mode == PersonOnEventsMode.DISABLED else "person_properties" + "properties" if self._person_on_events_mode == PersonsOnEventsMode.disabled else "person_properties" ) breakdown_values_expression, breakdown_values_params = get_single_or_multi_property_string_expr( @@ -149,7 +149,7 @@ def get_query(self) -> Tuple[str, Dict[str, Any]]: null_person_filter = ( f"AND notEmpty({self.EVENT_TABLE_ALIAS}.person_id)" - if self._person_on_events_mode != PersonOnEventsMode.DISABLED + if self._person_on_events_mode != PersonsOnEventsMode.disabled else "" ) @@ -197,7 +197,7 @@ def _determine_should_join_distinct_ids(self) -> None: self._filter.aggregation_group_type_index is not None or self._aggregate_users_by_distinct_id ) is_using_cohort_propertes = self._column_optimizer.is_using_cohort_propertes - if self._person_on_events_mode == PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS or ( + if self._person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events or ( non_person_id_aggregation and not is_using_cohort_propertes ): self._should_join_distinct_ids = False @@ -206,7 +206,7 @@ def _determine_should_join_distinct_ids(self) -> None: def _determine_should_join_persons(self) -> None: EventQuery._determine_should_join_persons(self) - if self._person_on_events_mode != PersonOnEventsMode.DISABLED: + if self._person_on_events_mode != PersonsOnEventsMode.disabled: self._should_join_persons = False def _get_entity_query(self, entity: Entity): diff --git a/posthog/queries/stickiness/stickiness_event_query.py b/posthog/queries/stickiness/stickiness_event_query.py index ce76b2d17ce5c..25d68b1d6bfdf 100644 --- a/posthog/queries/stickiness/stickiness_event_query.py +++ b/posthog/queries/stickiness/stickiness_event_query.py @@ -8,7 +8,7 @@ from posthog.queries.event_query import EventQuery from posthog.queries.person_query import PersonQuery from posthog.queries.util import get_person_properties_mode, get_start_of_interval_sql -from posthog.utils import PersonOnEventsMode +from posthog.schema import PersonsOnEventsMode class StickinessEventsQuery(EventQuery): @@ -43,7 +43,7 @@ def get_query(self) -> Tuple[str, Dict[str, Any]]: null_person_filter = ( f"AND notEmpty({self.EVENT_TABLE_ALIAS}.person_id)" - if self._person_on_events_mode != PersonOnEventsMode.DISABLED + if self._person_on_events_mode != PersonsOnEventsMode.disabled else "" ) @@ -82,14 +82,14 @@ def _person_query(self): ) def _determine_should_join_distinct_ids(self) -> None: - if self._person_on_events_mode == PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: + if self._person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: self._should_join_distinct_ids = False else: self._should_join_distinct_ids = True def _determine_should_join_persons(self) -> None: EventQuery._determine_should_join_persons(self) - if self._person_on_events_mode != PersonOnEventsMode.DISABLED: + if self._person_on_events_mode != PersonsOnEventsMode.disabled: self._should_join_persons = False def aggregation_target(self): diff --git a/posthog/queries/test/__snapshots__/test_trends.ambr b/posthog/queries/test/__snapshots__/test_trends.ambr index 89bc5d0b3d93a..bf9b5c6670f1f 100644 --- a/posthog/queries/test/__snapshots__/test_trends.ambr +++ b/posthog/queries/test/__snapshots__/test_trends.ambr @@ -5112,14 +5112,14 @@ FROM events WHERE team_id = 2 AND event = '$pageview' - AND toDateTime(timestamp, 'UTC') >= toDateTime('2020-01-01 23:59:59', 'UTC') - AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC')) + AND toDateTime(timestamp, 'UTC') >= toDateTime('2020-01-11 23:59:59', 'UTC') + AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-18 23:59:59', 'UTC')) GROUP BY distinct_id HAVING argMax(is_deleted, version) = 0) AS pdi ON e.distinct_id = pdi.distinct_id WHERE team_id = 2 AND event = '$pageview' - AND toDateTime(timestamp, 'UTC') >= toDateTime('2020-01-01 23:59:59', 'UTC') - AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') + AND toDateTime(timestamp, 'UTC') >= toDateTime('2020-01-11 23:59:59', 'UTC') + AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-18 23:59:59', 'UTC') ''' # --- # name: TestTrends.test_weekly_active_users_aggregated_range_wider_than_week_with_sampling @@ -5137,14 +5137,14 @@ FROM events WHERE team_id = 2 AND event = '$pageview' - AND toDateTime(timestamp, 'UTC') >= toDateTime('2020-01-01 23:59:59', 'UTC') - AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC')) + AND toDateTime(timestamp, 'UTC') >= toDateTime('2020-01-11 23:59:59', 'UTC') + AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-18 23:59:59', 'UTC')) GROUP BY distinct_id HAVING argMax(is_deleted, version) = 0) AS pdi ON e.distinct_id = pdi.distinct_id WHERE team_id = 2 AND event = '$pageview' - AND toDateTime(timestamp, 'UTC') >= toDateTime('2020-01-01 23:59:59', 'UTC') - AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') + AND toDateTime(timestamp, 'UTC') >= toDateTime('2020-01-11 23:59:59', 'UTC') + AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-18 23:59:59', 'UTC') ''' # --- # name: TestTrends.test_weekly_active_users_daily diff --git a/posthog/queries/test/test_trends.py b/posthog/queries/test/test_trends.py index 267e356ab0a23..abb32426dd68d 100644 --- a/posthog/queries/test/test_trends.py +++ b/posthog/queries/test/test_trends.py @@ -5893,7 +5893,7 @@ def test_weekly_active_users_aggregated_range_wider_than_week(self): data = { "date_from": "2020-01-01", - "date_to": "2020-01-08", + "date_to": "2020-01-18", "display": TRENDS_TABLE, "events": [ { @@ -5907,7 +5907,7 @@ def test_weekly_active_users_aggregated_range_wider_than_week(self): filter = Filter(team=self.team, data=data) result = Trends().run(filter, self.team) - # Only p0 was active on 2020-01-08 or in the preceding 6 days + # Only p0 was active on 2020-01-18 or in the preceding 6 days self.assertEqual(result[0]["aggregated_value"], 1) @snapshot_clickhouse_queries @@ -5917,7 +5917,7 @@ def test_weekly_active_users_aggregated_range_wider_than_week_with_sampling(self data = { "sampling_factor": 1, "date_from": "2020-01-01", - "date_to": "2020-01-08", + "date_to": "2020-01-18", "display": TRENDS_TABLE, "events": [ { @@ -5931,7 +5931,7 @@ def test_weekly_active_users_aggregated_range_wider_than_week_with_sampling(self filter = Filter(team=self.team, data=data) result = Trends().run(filter, self.team) - # Only p0 was active on 2020-01-08 or in the preceding 6 days + # Only p0 was active on 2020-01-18 or in the preceding 6 days self.assertEqual(result[0]["aggregated_value"], 1) @snapshot_clickhouse_queries diff --git a/posthog/queries/trends/breakdown.py b/posthog/queries/trends/breakdown.py index de8665a549610..444f045384a14 100644 --- a/posthog/queries/trends/breakdown.py +++ b/posthog/queries/trends/breakdown.py @@ -43,6 +43,7 @@ from posthog.queries.person_distinct_id_query import get_team_distinct_ids_query from posthog.queries.person_query import PersonQuery from posthog.queries.query_date_range import TIME_IN_SECONDS, QueryDateRange +from posthog.schema import PersonsOnEventsMode from posthog.session_recordings.queries.session_query import SessionQuery from posthog.queries.trends.sql import ( BREAKDOWN_ACTIVE_USER_AGGREGATE_SQL, @@ -76,11 +77,7 @@ get_person_properties_mode, get_start_of_interval_sql, ) -from posthog.utils import ( - PersonOnEventsMode, - encode_get_request_params, - generate_short_id, -) +from posthog.utils import encode_get_request_params, generate_short_id from posthog.queries.person_on_events_v2_sql import PERSON_OVERRIDES_JOIN_SQL BREAKDOWN_OTHER_STRING_LABEL = "$$_posthog_breakdown_other_$$" @@ -100,7 +97,7 @@ def __init__( filter: Filter, team: Team, column_optimizer: Optional[ColumnOptimizer] = None, - person_on_events_mode: PersonOnEventsMode = PersonOnEventsMode.DISABLED, + person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.disabled, add_person_urls: bool = False, ): self.entity = entity @@ -111,9 +108,9 @@ def __init__( self.column_optimizer = column_optimizer or ColumnOptimizer(self.filter, self.team_id) self.add_person_urls = add_person_urls self.person_on_events_mode = person_on_events_mode - if person_on_events_mode == PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: + if person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: self._person_id_alias = f"if(notEmpty({self.PERSON_ID_OVERRIDES_TABLE_ALIAS}.person_id), {self.PERSON_ID_OVERRIDES_TABLE_ALIAS}.person_id, {self.EVENT_TABLE_ALIAS}.person_id)" - elif person_on_events_mode == PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: + elif person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: self._person_id_alias = f"{self.EVENT_TABLE_ALIAS}.person_id" else: self._person_id_alias = f"{self.DISTINCT_ID_TABLE_ALIAS}.person_id" @@ -131,7 +128,7 @@ def _props_to_filter(self) -> Tuple[str, Dict]: ) target_properties: Optional[PropertyGroup] = props_to_filter - if self.person_on_events_mode == PersonOnEventsMode.DISABLED: + if self.person_on_events_mode == PersonsOnEventsMode.disabled: target_properties = self.column_optimizer.property_optimizer.parse_property_groups(props_to_filter).outer return parse_prop_grouped_clauses( @@ -163,7 +160,7 @@ def get_query(self) -> Tuple[str, Dict, Callable]: filter=self.filter, event_table_alias=self.EVENT_TABLE_ALIAS, person_id_alias=f"person_id" - if self.person_on_events_mode == PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS + if self.person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events else self._person_id_alias, ) @@ -200,7 +197,7 @@ def get_query(self) -> Tuple[str, Dict, Callable]: else "", "filters": prop_filters, "null_person_filter": f"AND notEmpty(e.person_id)" - if self.person_on_events_mode != PersonOnEventsMode.DISABLED + if self.person_on_events_mode != PersonsOnEventsMode.disabled else "", } @@ -522,7 +519,7 @@ def _get_breakdown_value(self, breakdown: str) -> str: raise ValidationError(f'Invalid breakdown "{breakdown}" for breakdown type "session"') elif ( - self.person_on_events_mode != PersonOnEventsMode.DISABLED + self.person_on_events_mode != PersonsOnEventsMode.disabled and self.filter.breakdown_type == "group" and groups_on_events_querying_enabled() ): @@ -534,7 +531,7 @@ def _get_breakdown_value(self, breakdown: str) -> str: properties_field, materialised_table_column=properties_field, ) - elif self.person_on_events_mode != PersonOnEventsMode.DISABLED and self.filter.breakdown_type != "group": + elif self.person_on_events_mode != PersonsOnEventsMode.disabled and self.filter.breakdown_type != "group": if self.filter.breakdown_type == "person": breakdown_value, _ = get_property_string_expr( "events", @@ -748,10 +745,10 @@ def _determine_breakdown_label( return str(value) or BREAKDOWN_NULL_DISPLAY def _person_join_condition(self) -> Tuple[str, Dict]: - if self.person_on_events_mode == PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: + if self.person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: return "", {} - if self.person_on_events_mode == PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: + if self.person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: return ( PERSON_OVERRIDES_JOIN_SQL.format( person_overrides_table_alias=self.PERSON_ID_OVERRIDES_TABLE_ALIAS, diff --git a/posthog/queries/trends/lifecycle.py b/posthog/queries/trends/lifecycle.py index 6397439d8922b..2629672879e7a 100644 --- a/posthog/queries/trends/lifecycle.py +++ b/posthog/queries/trends/lifecycle.py @@ -13,11 +13,8 @@ from posthog.queries.trends.sql import LIFECYCLE_EVENTS_QUERY, LIFECYCLE_SQL from posthog.queries.trends.util import parse_response from posthog.queries.util import get_person_properties_mode -from posthog.utils import ( - PersonOnEventsMode, - encode_get_request_params, - generate_short_id, -) +from posthog.schema import PersonsOnEventsMode +from posthog.utils import encode_get_request_params, generate_short_id # Lifecycle takes an event/action, time range, interval and for every period, splits the users who did the action into 4: # @@ -128,12 +125,12 @@ def get_query(self): self.params.update(entity_prop_params) created_at_clause = ( - "person.created_at" if self._person_on_events_mode == PersonOnEventsMode.DISABLED else "person_created_at" + "person.created_at" if self._person_on_events_mode == PersonsOnEventsMode.disabled else "person_created_at" ) null_person_filter = ( "" - if self._person_on_events_mode == PersonOnEventsMode.DISABLED + if self._person_on_events_mode == PersonsOnEventsMode.disabled else f"AND notEmpty({self.EVENT_TABLE_ALIAS}.person_id)" ) @@ -189,8 +186,8 @@ def _get_date_filter(self): def _determine_should_join_distinct_ids(self) -> None: self._should_join_distinct_ids = ( - self._person_on_events_mode != PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS + self._person_on_events_mode != PersonsOnEventsMode.person_id_no_override_properties_on_events ) def _determine_should_join_persons(self) -> None: - self._should_join_persons = self._person_on_events_mode == PersonOnEventsMode.DISABLED + self._should_join_persons = self._person_on_events_mode == PersonsOnEventsMode.disabled diff --git a/posthog/queries/trends/total_volume.py b/posthog/queries/trends/total_volume.py index 9c7977ca02592..e36f6d2de7313 100644 --- a/posthog/queries/trends/total_volume.py +++ b/posthog/queries/trends/total_volume.py @@ -38,16 +38,9 @@ parse_response, process_math, ) -from posthog.queries.util import ( - TIME_IN_SECONDS, - get_interval_func_ch, - get_start_of_interval_sql, -) -from posthog.utils import ( - PersonOnEventsMode, - encode_get_request_params, - generate_short_id, -) +from posthog.queries.util import TIME_IN_SECONDS, get_interval_func_ch, get_start_of_interval_sql +from posthog.schema import PersonsOnEventsMode +from posthog.utils import encode_get_request_params, generate_short_id class TrendsTotalVolume: @@ -59,9 +52,9 @@ def _total_volume_query(self, entity: Entity, filter: Filter, team: Team) -> Tup interval_func = get_interval_func_ch(filter.interval) person_id_alias = f"{self.DISTINCT_ID_TABLE_ALIAS}.person_id" - if team.person_on_events_mode == PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: + if team.person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: person_id_alias = f"if(notEmpty({self.PERSON_ID_OVERRIDES_TABLE_ALIAS}.person_id), {self.PERSON_ID_OVERRIDES_TABLE_ALIAS}.person_id, {self.EVENT_TABLE_ALIAS}.person_id)" - elif team.person_on_events_mode == PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: + elif team.person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: person_id_alias = f"{self.EVENT_TABLE_ALIAS}.person_id" aggregate_operation, join_condition, math_params = process_math( diff --git a/posthog/queries/trends/trends_actors.py b/posthog/queries/trends/trends_actors.py index 1648e7575e7b2..9c4afa89c41a6 100644 --- a/posthog/queries/trends/trends_actors.py +++ b/posthog/queries/trends/trends_actors.py @@ -16,7 +16,7 @@ is_series_group_based, process_math, ) -from posthog.utils import PersonOnEventsMode +from posthog.schema import PersonsOnEventsMode class TrendsActors(ActorBaseQuery): @@ -104,7 +104,7 @@ def actor_query(self, limit_actors: Optional[bool] = True) -> Tuple[str, Dict]: team=self._team, entity=self.entity, should_join_distinct_ids=not self.is_aggregating_by_groups - and self._team.person_on_events_mode != PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS, + and self._team.person_on_events_mode != PersonsOnEventsMode.person_id_no_override_properties_on_events, extra_event_properties=["$window_id", "$session_id"] if self._filter.include_recordings else [], extra_fields=extra_fields, person_on_events_mode=self._team.person_on_events_mode, diff --git a/posthog/queries/trends/trends_event_query.py b/posthog/queries/trends/trends_event_query.py index d3bcc9f381290..bc9e9b979bd00 100644 --- a/posthog/queries/trends/trends_event_query.py +++ b/posthog/queries/trends/trends_event_query.py @@ -2,7 +2,7 @@ from posthog.models.property.util import get_property_string_expr from posthog.queries.trends.trends_event_query_base import TrendsEventQueryBase -from posthog.utils import PersonOnEventsMode +from posthog.schema import PersonsOnEventsMode class TrendsEventQuery(TrendsEventQueryBase): @@ -10,7 +10,7 @@ def get_query(self) -> Tuple[str, Dict[str, Any]]: person_id_field = "" if self._should_join_distinct_ids: person_id_field = f", {self._person_id_alias} as person_id" - elif self._person_on_events_mode == PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: + elif self._person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: person_id_field = f", {self.EVENT_TABLE_ALIAS}.person_id as person_id" _fields = ( @@ -62,7 +62,7 @@ def get_query(self) -> Tuple[str, Dict[str, Any]]: return f"SELECT {_fields} {base_query}", params def _get_extra_person_columns(self) -> str: - if self._person_on_events_mode != PersonOnEventsMode.DISABLED: + if self._person_on_events_mode != PersonsOnEventsMode.disabled: return " ".join( ", {extract} as {column_name}".format( extract=get_property_string_expr( diff --git a/posthog/queries/trends/trends_event_query_base.py b/posthog/queries/trends/trends_event_query_base.py index 082aed8e556a0..dbeb9f17cdc3d 100644 --- a/posthog/queries/trends/trends_event_query_base.py +++ b/posthog/queries/trends/trends_event_query_base.py @@ -18,7 +18,7 @@ get_active_user_params, ) from posthog.queries.util import get_person_properties_mode -from posthog.utils import PersonOnEventsMode +from posthog.schema import PersonsOnEventsMode class TrendsEventQueryBase(EventQuery): @@ -80,7 +80,7 @@ def get_query_base(self) -> Tuple[str, Dict[str, Any]]: return query, self.params def _determine_should_join_distinct_ids(self) -> None: - if self._person_on_events_mode == PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: + if self._person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: self._should_join_distinct_ids = False is_entity_per_user = self._entity.math in ( @@ -97,7 +97,7 @@ def _determine_should_join_distinct_ids(self) -> None: self._should_join_distinct_ids = True def _determine_should_join_persons(self) -> None: - if self._person_on_events_mode != PersonOnEventsMode.DISABLED: + if self._person_on_events_mode != PersonsOnEventsMode.disabled: self._should_join_persons = False else: EventQuery._determine_should_join_persons(self) @@ -107,7 +107,7 @@ def _get_not_null_actor_condition(self) -> str: # If aggregating by person, exclude events with null/zero person IDs return ( f"AND notEmpty({self.EVENT_TABLE_ALIAS}.person_id)" - if self._person_on_events_mode != PersonOnEventsMode.DISABLED + if self._person_on_events_mode != PersonsOnEventsMode.disabled else "" ) else: diff --git a/posthog/queries/trends/util.py b/posthog/queries/trends/util.py index b91e4d6ac185d..bb11f0c38293d 100644 --- a/posthog/queries/trends/util.py +++ b/posthog/queries/trends/util.py @@ -23,7 +23,7 @@ from posthog.models.property.util import get_property_string_expr from posthog.models.team import Team from posthog.queries.util import correct_result_for_sampling, get_earliest_timestamp -from posthog.utils import PersonOnEventsMode +from posthog.schema import PersonsOnEventsMode logger = structlog.get_logger(__name__) @@ -174,9 +174,9 @@ def determine_aggregator(entity: Entity, team: Team) -> str: return f'"$group_{entity.math_group_type_index}"' elif team.aggregate_users_by_distinct_id: return "e.distinct_id" - elif team.person_on_events_mode == PersonOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: + elif team.person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: return "e.person_id" - elif team.person_on_events_mode == PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: + elif team.person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: return f"if(notEmpty(overrides.person_id), overrides.person_id, e.person_id)" else: return "pdi.person_id" diff --git a/posthog/queries/util.py b/posthog/queries/util.py index be08fb732c8ce..e366fb1cc7833 100644 --- a/posthog/queries/util.py +++ b/posthog/queries/util.py @@ -12,7 +12,7 @@ from posthog.models.team import Team from posthog.models.team.team import WeekStartDay from posthog.queries.insight import insight_sync_execute -from posthog.utils import PersonOnEventsMode +from posthog.schema import PersonsOnEventsMode class PersonPropertiesMode(Enum): @@ -175,10 +175,10 @@ def correct_result_for_sampling( def get_person_properties_mode(team: Team) -> PersonPropertiesMode: - if team.person_on_events_mode == PersonOnEventsMode.DISABLED: + if team.person_on_events_mode == PersonsOnEventsMode.disabled: return PersonPropertiesMode.USING_PERSON_PROPERTIES_COLUMN - if team.person_on_events_mode == PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: + if team.person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: return PersonPropertiesMode.DIRECT_ON_EVENTS_WITH_POE_V2 return PersonPropertiesMode.DIRECT_ON_EVENTS diff --git a/posthog/schema.py b/posthog/schema.py index d86343b88fee0..68471d4510b06 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -860,6 +860,7 @@ class StickinessQueryResponse(BaseModel): hogql: Optional[str] = None is_cached: Optional[bool] = None last_refresh: Optional[str] = None + modifiers: Optional[HogQLQueryModifiers] = None next_allowed_client_refresh: Optional[str] = None results: List[Dict[str, Any]] timings: Optional[List[QueryTiming]] = None @@ -870,6 +871,9 @@ class TimeToSeeDataQuery(BaseModel): extra="forbid", ) kind: Literal["TimeToSeeDataQuery"] = "TimeToSeeDataQuery" + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) response: Optional[Dict[str, Any]] = Field(default=None, description="Cached query response") sessionEnd: Optional[str] = None sessionId: Optional[str] = Field(default=None, description="Project to filter on. Defaults to current session") @@ -950,6 +954,7 @@ class TrendsQueryResponse(BaseModel): hogql: Optional[str] = None is_cached: Optional[bool] = None last_refresh: Optional[str] = None + modifiers: Optional[HogQLQueryModifiers] = None next_allowed_client_refresh: Optional[str] = None results: List[Dict[str, Any]] timings: Optional[List[QueryTiming]] = None @@ -1013,6 +1018,7 @@ class WebOverviewQueryResponse(BaseModel): hogql: Optional[str] = None is_cached: Optional[bool] = None last_refresh: Optional[str] = None + modifiers: Optional[HogQLQueryModifiers] = None next_allowed_client_refresh: Optional[str] = None results: List[WebOverviewItem] samplingRate: Optional[SamplingRate] = None @@ -1047,6 +1053,7 @@ class WebStatsTableQueryResponse(BaseModel): is_cached: Optional[bool] = None last_refresh: Optional[str] = None limit: Optional[int] = None + modifiers: Optional[HogQLQueryModifiers] = None next_allowed_client_refresh: Optional[str] = None offset: Optional[int] = None results: List @@ -1063,6 +1070,7 @@ class WebTopClicksQueryResponse(BaseModel): hogql: Optional[str] = None is_cached: Optional[bool] = None last_refresh: Optional[str] = None + modifiers: Optional[HogQLQueryModifiers] = None next_allowed_client_refresh: Optional[str] = None results: List samplingRate: Optional[SamplingRate] = None @@ -1079,6 +1087,7 @@ class ActorsQueryResponse(BaseModel): hogql: str limit: int missing_actors_count: Optional[int] = None + modifiers: Optional[HogQLQueryModifiers] = None offset: int results: List[List] timings: Optional[List[QueryTiming]] = None @@ -1121,6 +1130,9 @@ class DataNode(BaseModel): extra="forbid", ) kind: NodeKind + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) response: Optional[Dict[str, Any]] = Field(default=None, description="Cached query response") @@ -1185,6 +1197,7 @@ class EventsQueryResponse(BaseModel): hasMore: Optional[bool] = None hogql: str limit: Optional[int] = None + modifiers: Optional[HogQLQueryModifiers] = None offset: Optional[int] = None results: List[List] timings: Optional[List[QueryTiming]] = None @@ -1210,6 +1223,7 @@ class FunnelCorrelationResponse(BaseModel): hasMore: Optional[bool] = None hogql: Optional[str] = None limit: Optional[int] = None + modifiers: Optional[HogQLQueryModifiers] = None offset: Optional[int] = None results: FunnelCorrelationResult timings: Optional[List[QueryTiming]] = None @@ -1243,6 +1257,7 @@ class FunnelsQueryResponse(BaseModel): hogql: Optional[str] = None is_cached: Optional[bool] = None last_refresh: Optional[str] = None + modifiers: Optional[HogQLQueryModifiers] = None next_allowed_client_refresh: Optional[str] = None results: Union[FunnelTimeToConvertResults, List[Dict[str, Any]], List[List[Dict[str, Any]]]] timings: Optional[List[QueryTiming]] = None @@ -1325,6 +1340,7 @@ class InsightActorsQueryBase(BaseModel): extra="forbid", ) includeRecordings: Optional[bool] = None + modifiers: Optional[HogQLQueryModifiers] = None response: Optional[ActorsQueryResponse] = None @@ -1351,6 +1367,7 @@ class LifecycleQueryResponse(BaseModel): hogql: Optional[str] = None is_cached: Optional[bool] = None last_refresh: Optional[str] = None + modifiers: Optional[HogQLQueryModifiers] = None next_allowed_client_refresh: Optional[str] = None results: List[Dict[str, Any]] timings: Optional[List[QueryTiming]] = None @@ -1370,6 +1387,7 @@ class PathsQueryResponse(BaseModel): hogql: Optional[str] = None is_cached: Optional[bool] = None last_refresh: Optional[str] = None + modifiers: Optional[HogQLQueryModifiers] = None next_allowed_client_refresh: Optional[str] = None results: List[Dict[str, Any]] timings: Optional[List[QueryTiming]] = None @@ -1393,6 +1411,7 @@ class QueryResponse(BaseModel): hogql: Optional[str] = None is_cached: Optional[bool] = None last_refresh: Optional[str] = None + modifiers: Optional[HogQLQueryModifiers] = None next_allowed_client_refresh: Optional[str] = None results: Any timings: Optional[List[QueryTiming]] = None @@ -1406,6 +1425,7 @@ class QueryResponseAlternative3(BaseModel): hasMore: Optional[bool] = None hogql: str limit: Optional[int] = None + modifiers: Optional[HogQLQueryModifiers] = None offset: Optional[int] = None results: List[List] timings: Optional[List[QueryTiming]] = None @@ -1421,6 +1441,7 @@ class QueryResponseAlternative4(BaseModel): hogql: str limit: int missing_actors_count: Optional[int] = None + modifiers: Optional[HogQLQueryModifiers] = None offset: int results: List[List] timings: Optional[List[QueryTiming]] = None @@ -1481,6 +1502,7 @@ class QueryResponseAlternative10(BaseModel): hogql: Optional[str] = None is_cached: Optional[bool] = None last_refresh: Optional[str] = None + modifiers: Optional[HogQLQueryModifiers] = None next_allowed_client_refresh: Optional[str] = None results: List[WebOverviewItem] samplingRate: Optional[SamplingRate] = None @@ -1497,6 +1519,7 @@ class QueryResponseAlternative11(BaseModel): is_cached: Optional[bool] = None last_refresh: Optional[str] = None limit: Optional[int] = None + modifiers: Optional[HogQLQueryModifiers] = None next_allowed_client_refresh: Optional[str] = None offset: Optional[int] = None results: List @@ -1513,6 +1536,7 @@ class QueryResponseAlternative12(BaseModel): hogql: Optional[str] = None is_cached: Optional[bool] = None last_refresh: Optional[str] = None + modifiers: Optional[HogQLQueryModifiers] = None next_allowed_client_refresh: Optional[str] = None results: List samplingRate: Optional[SamplingRate] = None @@ -1527,6 +1551,7 @@ class QueryResponseAlternative13(BaseModel): hogql: Optional[str] = None is_cached: Optional[bool] = None last_refresh: Optional[str] = None + modifiers: Optional[HogQLQueryModifiers] = None next_allowed_client_refresh: Optional[str] = None results: List[Dict[str, Any]] timings: Optional[List[QueryTiming]] = None @@ -1540,6 +1565,7 @@ class QueryResponseAlternative17(BaseModel): hasMore: Optional[bool] = None hogql: Optional[str] = None limit: Optional[int] = None + modifiers: Optional[HogQLQueryModifiers] = None offset: Optional[int] = None results: FunnelCorrelationResult timings: Optional[List[QueryTiming]] = None @@ -1660,6 +1686,9 @@ class TimeToSeeDataSessionsQuery(BaseModel): ) dateRange: Optional[DateRange] = Field(default=None, description="Date range for the query") kind: Literal["TimeToSeeDataSessionsQuery"] = "TimeToSeeDataSessionsQuery" + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) response: Optional[TimeToSeeDataSessionsQueryResponse] = Field(default=None, description="Cached query response") teamId: Optional[int] = Field(default=None, description="Project to filter on. Defaults to current project") @@ -1669,6 +1698,7 @@ class WebAnalyticsQueryBase(BaseModel): extra="forbid", ) dateRange: Optional[DateRange] = None + modifiers: Optional[HogQLQueryModifiers] = None properties: List[Union[EventPropertyFilter, PersonPropertyFilter]] sampling: Optional[Sampling] = None useSessionsTable: Optional[bool] = None @@ -1681,6 +1711,7 @@ class WebOverviewQuery(BaseModel): compare: Optional[bool] = None dateRange: Optional[DateRange] = None kind: Literal["WebOverviewQuery"] = "WebOverviewQuery" + modifiers: Optional[HogQLQueryModifiers] = None properties: List[Union[EventPropertyFilter, PersonPropertyFilter]] response: Optional[WebOverviewQueryResponse] = None sampling: Optional[Sampling] = None @@ -1698,6 +1729,7 @@ class WebStatsTableQuery(BaseModel): includeScrollDepth: Optional[bool] = None kind: Literal["WebStatsTableQuery"] = "WebStatsTableQuery" limit: Optional[int] = None + modifiers: Optional[HogQLQueryModifiers] = None properties: List[Union[EventPropertyFilter, PersonPropertyFilter]] response: Optional[WebStatsTableQueryResponse] = None sampling: Optional[Sampling] = None @@ -1710,6 +1742,7 @@ class WebTopClicksQuery(BaseModel): ) dateRange: Optional[DateRange] = None kind: Literal["WebTopClicksQuery"] = "WebTopClicksQuery" + modifiers: Optional[HogQLQueryModifiers] = None properties: List[Union[EventPropertyFilter, PersonPropertyFilter]] response: Optional[WebTopClicksQueryResponse] = None sampling: Optional[Sampling] = None @@ -1800,6 +1833,9 @@ class DataWarehouseNode(BaseModel): math_group_type_index: Optional[MathGroupTypeIndex] = None math_hogql: Optional[str] = None math_property: Optional[str] = None + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) name: Optional[str] = None properties: Optional[ List[ @@ -1829,6 +1865,9 @@ class DatabaseSchemaQuery(BaseModel): extra="forbid", ) kind: Literal["DatabaseSchemaQuery"] = "DatabaseSchemaQuery" + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) response: Optional[Dict[str, List[DatabaseSchemaQueryResponseField]]] = Field( default=None, description="Cached query response" ) @@ -1867,6 +1906,9 @@ class EntityNode(BaseModel): math_group_type_index: Optional[MathGroupTypeIndex] = None math_hogql: Optional[str] = None math_property: Optional[str] = None + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) name: Optional[str] = None properties: Optional[ List[ @@ -1924,6 +1966,9 @@ class EventsNode(BaseModel): math_group_type_index: Optional[MathGroupTypeIndex] = None math_hogql: Optional[str] = None math_property: Optional[str] = None + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) name: Optional[str] = None orderBy: Optional[List[str]] = Field(default=None, description="Columns to order by") properties: Optional[ @@ -1979,6 +2024,9 @@ class EventsQuery(BaseModel): ) kind: Literal["EventsQuery"] = "EventsQuery" limit: Optional[int] = Field(default=None, description="Number of rows to return") + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) offset: Optional[int] = Field(default=None, description="Number of rows to skip before returning rows") orderBy: Optional[List[str]] = Field(default=None, description="Columns to order by") personId: Optional[str] = Field(default=None, description="Show events for a given person") @@ -2041,6 +2089,9 @@ class FunnelExclusionActionsNode(BaseModel): math_group_type_index: Optional[MathGroupTypeIndex] = None math_hogql: Optional[str] = None math_property: Optional[str] = None + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) name: Optional[str] = None properties: Optional[ List[ @@ -2100,6 +2151,9 @@ class FunnelExclusionEventsNode(BaseModel): math_group_type_index: Optional[MathGroupTypeIndex] = None math_hogql: Optional[str] = None math_property: Optional[str] = None + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) name: Optional[str] = None orderBy: Optional[List[str]] = Field(default=None, description="Columns to order by") properties: Optional[ @@ -2156,7 +2210,9 @@ class HogQLQuery(BaseModel): explain: Optional[bool] = None filters: Optional[HogQLFilters] = None kind: Literal["HogQLQuery"] = "HogQLQuery" - modifiers: Optional[HogQLQueryModifiers] = None + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) query: str response: Optional[HogQLQueryResponse] = Field(default=None, description="Cached query response") values: Optional[Dict[str, Any]] = Field( @@ -2193,6 +2249,9 @@ class PersonsNode(BaseModel): ) kind: Literal["PersonsNode"] = "PersonsNode" limit: Optional[int] = None + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) offset: Optional[int] = None properties: Optional[ List[ @@ -2249,6 +2308,7 @@ class QueryResponseAlternative14(BaseModel): hogql: Optional[str] = None is_cached: Optional[bool] = None last_refresh: Optional[str] = None + modifiers: Optional[HogQLQueryModifiers] = None next_allowed_client_refresh: Optional[str] = None results: List[RetentionResult] timings: Optional[List[QueryTiming]] = None @@ -2305,6 +2365,7 @@ class RetentionQueryResponse(BaseModel): hogql: Optional[str] = None is_cached: Optional[bool] = None last_refresh: Optional[str] = None + modifiers: Optional[HogQLQueryModifiers] = None next_allowed_client_refresh: Optional[str] = None results: List[RetentionResult] timings: Optional[List[QueryTiming]] = None @@ -2321,6 +2382,9 @@ class SessionsTimelineQuery(BaseModel): default=None, description="Only fetch sessions that started before this timestamp (default: '+5s')" ) kind: Literal["SessionsTimelineQuery"] = "SessionsTimelineQuery" + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) personId: Optional[str] = Field(default=None, description="Fetch sessions only for a given person") response: Optional[SessionsTimelineQueryResponse] = Field(default=None, description="Cached query response") @@ -2359,6 +2423,9 @@ class ActionsNode(BaseModel): math_group_type_index: Optional[MathGroupTypeIndex] = None math_hogql: Optional[str] = None math_property: Optional[str] = None + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) name: Optional[str] = None properties: Optional[ List[ @@ -2422,6 +2489,9 @@ class HogQLAutocomplete(BaseModel): endPosition: int = Field(..., description="End position of the editor word") filters: Optional[HogQLFilters] = Field(default=None, description="Table to validate the expression against") kind: Literal["HogQLAutocomplete"] = "HogQLAutocomplete" + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) response: Optional[HogQLAutocompleteResponse] = Field(default=None, description="Cached query response") select: str = Field(..., description="Full select query to validate") startPosition: int = Field(..., description="Start position of the editor word") @@ -2451,6 +2521,9 @@ class RetentionQuery(BaseModel): default=None, description="Exclude internal and test users by applying the respective filters" ) kind: Literal["RetentionQuery"] = "RetentionQuery" + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) properties: Optional[ Union[ List[ @@ -2489,6 +2562,9 @@ class StickinessQuery(BaseModel): default=None, description="Granularity of the response. Can be one of `hour`, `day`, `week` or `month`" ) kind: Literal["StickinessQuery"] = "StickinessQuery" + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) properties: Optional[ Union[ List[ @@ -2533,6 +2609,9 @@ class TrendsQuery(BaseModel): default=None, description="Granularity of the response. Can be one of `hour`, `day`, `week` or `month`" ) kind: Literal["TrendsQuery"] = "TrendsQuery" + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) properties: Optional[ Union[ List[ @@ -2632,6 +2711,9 @@ class FunnelsQuery(BaseModel): default=None, description="Granularity of the response. Can be one of `hour`, `day`, `week` or `month`" ) kind: Literal["FunnelsQuery"] = "FunnelsQuery" + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) properties: Optional[ Union[ List[ @@ -2669,6 +2751,9 @@ class InsightsQueryBase(BaseModel): default=None, description="Exclude internal and test users by applying the respective filters" ) kind: NodeKind + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) properties: Optional[ Union[ List[ @@ -2708,6 +2793,9 @@ class LifecycleQuery(BaseModel): lifecycleFilter: Optional[LifecycleFilter] = Field( default=None, description="Properties specific to the lifecycle insight" ) + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) properties: Optional[ Union[ List[ @@ -2775,6 +2863,7 @@ class FunnelsActorsQuery(BaseModel): ) includeRecordings: Optional[bool] = None kind: Literal["FunnelsActorsQuery"] = "FunnelsActorsQuery" + modifiers: Optional[HogQLQueryModifiers] = None response: Optional[ActorsQueryResponse] = None source: FunnelsQuery @@ -2792,6 +2881,9 @@ class PathsQuery(BaseModel): default=None, description="Used for displaying paths in relation to funnel steps." ) kind: Literal["PathsQuery"] = "PathsQuery" + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) pathsFilter: PathsFilter = Field(..., description="Properties specific to the paths insight") properties: Optional[ Union[ @@ -2883,6 +2975,7 @@ class FunnelCorrelationActorsQuery(BaseModel): ] = None includeRecordings: Optional[bool] = None kind: Literal["FunnelCorrelationActorsQuery"] = "FunnelCorrelationActorsQuery" + modifiers: Optional[HogQLQueryModifiers] = None response: Optional[ActorsQueryResponse] = None source: FunnelCorrelationQuery @@ -2899,6 +2992,7 @@ class InsightActorsQuery(BaseModel): default=None, description="An interval selected out of available intervals in source query." ) kind: Literal["InsightActorsQuery"] = "InsightActorsQuery" + modifiers: Optional[HogQLQueryModifiers] = None response: Optional[ActorsQueryResponse] = None series: Optional[int] = None source: Union[TrendsQuery, FunnelsQuery, RetentionQuery, PathsQuery, StickinessQuery, LifecycleQuery] = Field( @@ -2940,6 +3034,9 @@ class ActorsQuery(BaseModel): ] = None kind: Literal["ActorsQuery"] = "ActorsQuery" limit: Optional[int] = None + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) offset: Optional[int] = None orderBy: Optional[List[str]] = None properties: Optional[ @@ -3054,6 +3151,9 @@ class HogQLMetadata(BaseModel): ] = Field(default=None, description='Query within which "expr" is validated. Defaults to "select * from events"') filters: Optional[HogQLFilters] = Field(default=None, description="Extra filters applied to query via {filters}") kind: Literal["HogQLMetadata"] = "HogQLMetadata" + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) response: Optional[HogQLMetadataResponse] = Field(default=None, description="Cached query response") select: Optional[str] = Field( default=None, description="Full select query to validate (use `select` or `expr`, but not both)" diff --git a/posthog/session_recordings/queries/session_recording_list_from_replay_summary.py b/posthog/session_recordings/queries/session_recording_list_from_replay_summary.py index 4ff24160ae3e6..4f64fff7f8ab3 100644 --- a/posthog/session_recordings/queries/session_recording_list_from_replay_summary.py +++ b/posthog/session_recordings/queries/session_recording_list_from_replay_summary.py @@ -14,7 +14,7 @@ from posthog.models.filters.session_recordings_filter import SessionRecordingsFilter from posthog.models.property import PropertyGroup from posthog.models.property.util import parse_prop_grouped_clauses -from posthog.models.team import PersonOnEventsMode +from posthog.models.team import PersonsOnEventsMode from posthog.queries.event_query import EventQuery from posthog.queries.util import PersonPropertiesMode from posthog.session_recordings.queries.session_replay_events import ttl_days @@ -197,7 +197,7 @@ def _data_to_return(self, results: List[Any]) -> List[Dict[str, Any]]: def get_query(self) -> Tuple[str, Dict[str, Any]]: # we don't support PoE V1 - hopefully that's ok - if self._person_on_events_mode == PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: + if self._person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: return "", {} prop_query, prop_params = self._get_prop_groups( @@ -302,7 +302,7 @@ def _determine_should_join_events(self): ) has_poe_filters = ( - self._person_on_events_mode == PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS + self._person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events and len( [ pg @@ -314,7 +314,7 @@ def _determine_should_join_events(self): ) has_poe_person_filter = ( - self._person_on_events_mode == PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS + self._person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events and self._filter.person_uuid ) @@ -371,7 +371,7 @@ def format_event_filter(self, entity: Entity, prepend: str, team_id: int) -> Tup allow_denormalized_props=True, has_person_id_joined=True, person_properties_mode=PersonPropertiesMode.DIRECT_ON_EVENTS_WITH_POE_V2 - if self._person_on_events_mode == PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS + if self._person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events else PersonPropertiesMode.USING_PERSON_PROPERTIES_COLUMN, hogql_context=self._filter.hogql_context, ) @@ -416,7 +416,7 @@ def build_event_filters(self) -> SummaryEventFiltersSQL: -- select the unique events in this session to support filtering sessions by presence of an event groupUniqArray(event) as event_names,""" - if self._person_on_events_mode == PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: + if self._person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: person_id_clause, person_id_params = self._get_person_id_clause condition_sql += person_id_clause params = {**params, **person_id_params} @@ -493,7 +493,7 @@ def get_query(self, select_event_ids: bool = False) -> Tuple[str, Dict[str, Any] g for g in self._filter.property_groups.flat if ( - self._person_on_events_mode == PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS + self._person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events and g.type == "person" ) or ( @@ -509,7 +509,7 @@ def get_query(self, select_event_ids: bool = False) -> Tuple[str, Dict[str, Any] # but would need careful monitoring allow_denormalized_props=settings.ALLOW_DENORMALIZED_PROPS_IN_LISTING, person_properties_mode=PersonPropertiesMode.DIRECT_ON_EVENTS_WITH_POE_V2 - if self._person_on_events_mode == PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS + if self._person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events else PersonPropertiesMode.USING_PERSON_PROPERTIES_COLUMN, ) diff --git a/posthog/tasks/exports/csv_exporter.py b/posthog/tasks/exports/csv_exporter.py index 7fa226ee0026a..1030bb84a96ea 100644 --- a/posthog/tasks/exports/csv_exporter.py +++ b/posthog/tasks/exports/csv_exporter.py @@ -86,7 +86,10 @@ def _convert_response_to_csv_data(data: Any) -> Generator[Any, None, None]: for row in results: row_dict = {} for idx, x in enumerate(row): - row_dict[data["columns"][idx]] = x + if not data.get("columns"): + row_dict[f"column_{idx}"] = x + else: + row_dict[data["columns"][idx]] = x yield row_dict return @@ -285,6 +288,9 @@ def _export_to_dict(exported_asset: ExportedAsset, limit: int) -> Any: if not is_any_col_list_or_dict: # If values are serialised then keep the order of the keys, else allow it to be unordered renderer.header = all_csv_rows[0].keys() + else: + # If we have no rows, that means we couldn't convert anything, so put something to avoid confusion + all_csv_rows = [{"error": "No data available or unable to format for export."}] return renderer, all_csv_rows, render_context diff --git a/posthog/tasks/exports/test/test_csv_exporter.py b/posthog/tasks/exports/test/test_csv_exporter.py index 2da720b1bb958..87d731dd6a192 100644 --- a/posthog/tasks/exports/test/test_csv_exporter.py +++ b/posthog/tasks/exports/test/test_csv_exporter.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import Any, Dict, Optional from unittest import mock from unittest.mock import MagicMock, Mock, patch, ANY @@ -28,7 +29,8 @@ add_query_params, ) from posthog.hogql.constants import CSV_EXPORT_BREAKDOWN_LIMIT_INITIAL -from posthog.test.base import APIBaseTest, _create_event, flush_persons_and_events +from posthog.test.base import APIBaseTest, _create_event, flush_persons_and_events, _create_person +from posthog.test.test_journeys import journeys_for from posthog.utils import absolute_uri TEST_PREFIX = "Test-Exports" @@ -497,6 +499,95 @@ def test_csv_exporter_events_query_with_columns( self.assertEqual(first_row[1], "$pageview") self.assertEqual(first_row[4], str(self.team.pk)) + @patch("posthog.hogql.constants.MAX_SELECT_RETURNED_ROWS", 10) + @patch("posthog.models.exported_asset.UUIDT") + def test_csv_exporter_funnels_query(self, mocked_uuidt: Any, MAX_SELECT_RETURNED_ROWS: int = 10) -> None: + _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) + flush_persons_and_events() + + exported_asset = ExportedAsset( + team=self.team, + export_format=ExportedAsset.ExportFormat.CSV, + export_context={ + "source": { + "kind": "FunnelsQuery", + "series": [ + {"kind": "EventsNode", "name": "$pageview", "event": "$pageview"}, + {"kind": "EventsNode", "name": "$pageview", "event": "$pageview"}, + ], + "interval": "day", + "dateRange": {"date_to": "2024-03-22", "date_from": "2024-03-22"}, + "funnelsFilter": {"funnelVizType": "steps"}, + "breakdownFilter": {"breakdown": "utm_medium", "breakdown_type": "event"}, + } + }, + ) + exported_asset.save() + mocked_uuidt.return_value = "a-guid" + + with self.settings(OBJECT_STORAGE_ENABLED=True, OBJECT_STORAGE_EXPORTS_FOLDER="Test-Exports"): + csv_exporter.export_tabular(exported_asset) + content = object_storage.read(exported_asset.content_location) + lines = (content or "").split("\r\n") + self.assertEqual(len(lines), 3) + self.assertEqual( + lines[0], + "column_0.action_id,column_0.name,column_0.custom_name,column_0.order,column_0.count,column_0.type,column_0.average_conversion_time,column_0.median_conversion_time,column_0.breakdown.0,column_0.breakdown_value.0,column_1.action_id,column_1.name,column_1.custom_name,column_1.order,column_1.count,column_1.type,column_1.average_conversion_time,column_1.median_conversion_time,column_1.breakdown.0,column_1.breakdown_value.0", + ) + first_row = lines[1].split(",") + self.assertEqual(first_row[0], "$pageview") + + @patch("posthog.models.exported_asset.UUIDT") + def test_csv_exporter_empty_result(self, mocked_uuidt: Any) -> None: + exported_asset = ExportedAsset( + team=self.team, + export_format=ExportedAsset.ExportFormat.CSV, + export_context={ + "source": { + "kind": "FunnelsQuery", + "series": [ + {"kind": "EventsNode", "name": "$pageview", "event": "$pageview"}, + {"kind": "EventsNode", "name": "$pageview", "event": "$pageview"}, + ], + "interval": "day", + "dateRange": {"date_to": "2024-03-22", "date_from": "2024-03-22"}, + "funnelsFilter": {"funnelVizType": "steps"}, + "breakdownFilter": {"breakdown": "utm_medium", "breakdown_type": "event"}, + } + }, + ) + exported_asset.save() + mocked_uuidt.return_value = "a-guid" + + with patch("posthog.tasks.exports.csv_exporter.get_from_hogql_query") as mocked_get_from_hogql_query: + mocked_get_from_hogql_query.return_value = iter([]) + + with self.settings(OBJECT_STORAGE_ENABLED=True, OBJECT_STORAGE_EXPORTS_FOLDER="Test-Exports"): + csv_exporter.export_tabular(exported_asset) + content = object_storage.read(exported_asset.content_location) + lines = (content or "").split("\r\n") + self.assertEqual(lines[0], "error") + self.assertEqual(lines[1], "No data available or unable to format for export.") + def _split_to_dict(self, url: str) -> Dict[str, Any]: first_split_parts = url.split("?") assert len(first_split_parts) == 2 diff --git a/posthog/tasks/warehouse.py b/posthog/tasks/warehouse.py index f9c526ac19616..ff57a3aa764c6 100644 --- a/posthog/tasks/warehouse.py +++ b/posthog/tasks/warehouse.py @@ -45,7 +45,7 @@ def check_synced_row_limits_of_team(team_id: int) -> None: logger.exception("Could not cancel external data workflow", exc_info=e) try: - pause_external_data_schedule(job.pipeline) + pause_external_data_schedule(str(job.pipeline.id)) except Exception as e: logger.exception("Could not pause external data schedule", exc_info=e) @@ -58,7 +58,7 @@ def check_synced_row_limits_of_team(team_id: int) -> None: all_sources = ExternalDataSource.objects.filter(team_id=team_id, status=ExternalDataSource.Status.PAUSED) for source in all_sources: try: - unpause_external_data_schedule(source) + unpause_external_data_schedule(str(source.id)) except Exception as e: logger.exception("Could not unpause external data schedule", exc_info=e) diff --git a/posthog/temporal/common/schedule.py b/posthog/temporal/common/schedule.py index 7e2ae334a73bc..cb6e3bcccabd2 100644 --- a/posthog/temporal/common/schedule.py +++ b/posthog/temporal/common/schedule.py @@ -93,6 +93,16 @@ async def a_trigger_schedule(temporal: Client, schedule_id: str, note: str | Non await handle.trigger() +@async_to_sync +async def schedule_exists(temporal: Client, schedule_id: str) -> bool: + """Check whether a schedule exists.""" + try: + await temporal.get_schedule_handle(schedule_id).describe() + return True + except: + return False + + async def a_schedule_exists(temporal: Client, schedule_id: str) -> bool: """Check whether a schedule exists.""" try: diff --git a/posthog/temporal/data_imports/external_data_job.py b/posthog/temporal/data_imports/external_data_job.py index 938ab423a0cbe..62a47092d81bd 100644 --- a/posthog/temporal/data_imports/external_data_job.py +++ b/posthog/temporal/data_imports/external_data_job.py @@ -32,7 +32,6 @@ ExternalDataJob, get_active_schemas_for_source_id, ExternalDataSource, - aget_schema_by_id, ) from posthog.temporal.common.logger import bind_temporal_worker_logger from typing import Dict @@ -120,12 +119,6 @@ async def check_schedule_activity(inputs: ExternalDataWorkflowInputs) -> bool: logger.info(f"Deleted schedule for source {inputs.external_data_source_id}") return True - schema_model = await aget_schema_by_id(inputs.external_data_schema_id, inputs.team_id) - - # schema turned off so don't sync - if schema_model and not schema_model.should_sync: - return True - logger.info("Schema ID is set. Continuing...") return False diff --git a/posthog/test/test_team.py b/posthog/test/test_team.py index c6e9a681b4839..ea40559b48e69 100644 --- a/posthog/test/test_team.py +++ b/posthog/test/test_team.py @@ -15,7 +15,7 @@ from posthog.models.project import Project from posthog.models.team import get_team_in_cache, util from posthog.plugins.test.mock import mocked_plugin_requests_get -from posthog.utils import PersonOnEventsMode +from posthog.schema import PersonsOnEventsMode from .base import BaseTest @@ -138,7 +138,9 @@ def test_team_on_cloud_uses_feature_flag_to_determine_person_on_events(self, moc with self.is_cloud(True): with override_instance_config("PERSON_ON_EVENTS_ENABLED", False): team = Team.objects.create_with_data(organization=self.organization) - self.assertEqual(team.person_on_events_mode, PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS) + self.assertEqual( + team.person_on_events_mode, PersonsOnEventsMode.person_id_override_properties_on_events + ) # called more than once when evaluating hogql mock_feature_enabled.assert_called_with( "persons-on-events-v2-reads-enabled", @@ -159,12 +161,14 @@ def test_team_on_self_hosted_uses_instance_setting_to_determine_person_on_events with self.is_cloud(False): with override_instance_config("PERSON_ON_EVENTS_V2_ENABLED", True): team = Team.objects.create_with_data(organization=self.organization) - self.assertEqual(team.person_on_events_mode, PersonOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS) + self.assertEqual( + team.person_on_events_mode, PersonsOnEventsMode.person_id_override_properties_on_events + ) mock_feature_enabled.assert_not_called() with override_instance_config("PERSON_ON_EVENTS_V2_ENABLED", False): team = Team.objects.create_with_data(organization=self.organization) - self.assertEqual(team.person_on_events_mode, PersonOnEventsMode.DISABLED) + self.assertEqual(team.person_on_events_mode, PersonsOnEventsMode.disabled) mock_feature_enabled.assert_not_called() def test_each_team_gets_project_with_default_name_and_same_id(self): diff --git a/posthog/utils.py b/posthog/utils.py index 72c8d73d988fd..f186fdadb4adb 100644 --- a/posthog/utils.py +++ b/posthog/utils.py @@ -1304,13 +1304,6 @@ def patch(wrapper): return inner -class PersonOnEventsMode(str, Enum): - DISABLED = "disabled" - PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS = "person_id_no_override_properties_on_events" - PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS = "person_id_override_properties_on_events" - PERSON_ID_OVERRIDE_PROPERTIES_JOINED = "person_id_override_properties_joined" - - def label_for_team_id_to_track(team_id: int) -> str: team_id_filter: List[str] = settings.DECIDE_TRACK_TEAM_IDS diff --git a/posthog/warehouse/api/external_data_schema.py b/posthog/warehouse/api/external_data_schema.py index db6b23a1e9c71..41cd9bff2dbdc 100644 --- a/posthog/warehouse/api/external_data_schema.py +++ b/posthog/warehouse/api/external_data_schema.py @@ -6,6 +6,12 @@ from rest_framework.exceptions import NotAuthenticated from posthog.models import User from posthog.hogql.database.database import create_hogql_database +from posthog.warehouse.data_load.service import ( + external_data_workflow_exists, + sync_external_data_job_workflow, + pause_external_data_schedule, + unpause_external_data_schedule, +) class ExternalDataSchemaSerializer(serializers.ModelSerializer): @@ -25,6 +31,21 @@ def get_table(self, schema: ExternalDataSchema) -> Optional[dict]: return SimpleTableSerializer(schema.table, context={"database": hogql_context}).data or None + def update(self, instance: ExternalDataSchema, validated_data: Dict[str, Any]) -> ExternalDataSchema: + should_sync = validated_data.get("should_sync", None) + schedule_exists = external_data_workflow_exists(str(instance.id)) + + if schedule_exists: + if should_sync is False: + pause_external_data_schedule(str(instance.id)) + elif should_sync is True: + unpause_external_data_schedule(str(instance.id)) + else: + if should_sync is True: + sync_external_data_job_workflow(instance, create=True) + + return super().update(instance, validated_data) + class SimpleExternalDataSchemaSerializer(serializers.ModelSerializer): class Meta: diff --git a/posthog/warehouse/data_load/service.py b/posthog/warehouse/data_load/service.py index 31fc1aab6baff..688ee42b788b5 100644 --- a/posthog/warehouse/data_load/service.py +++ b/posthog/warehouse/data_load/service.py @@ -21,6 +21,7 @@ create_schedule, pause_schedule, a_schedule_exists, + schedule_exists, trigger_schedule, update_schedule, delete_schedule, @@ -110,19 +111,24 @@ async def a_trigger_external_data_workflow(external_data_schema: ExternalDataSch await a_trigger_schedule(temporal, schedule_id=str(external_data_schema.id)) +def external_data_workflow_exists(id: str) -> bool: + temporal = sync_connect() + return schedule_exists(temporal, schedule_id=id) + + async def a_external_data_workflow_exists(id: str) -> bool: temporal = await async_connect() return await a_schedule_exists(temporal, schedule_id=id) -def pause_external_data_schedule(external_data_source: ExternalDataSource): +def pause_external_data_schedule(id: str): temporal = sync_connect() - pause_schedule(temporal, schedule_id=str(external_data_source.id)) + pause_schedule(temporal, schedule_id=id) -def unpause_external_data_schedule(external_data_source: ExternalDataSource): +def unpause_external_data_schedule(id: str): temporal = sync_connect() - unpause_schedule(temporal, schedule_id=str(external_data_source.id)) + unpause_schedule(temporal, schedule_id=id) def delete_external_data_schedule(schedule_id: str):