diff --git a/cypress/e2e/trends.cy.ts b/cypress/e2e/trends.cy.ts index 06878284c9026..81dd84b67145e 100644 --- a/cypress/e2e/trends.cy.ts +++ b/cypress/e2e/trends.cy.ts @@ -44,6 +44,22 @@ describe('Trends', () => { cy.get('[data-attr=math-property-select]').should('exist') }) + it('Select HogQL expressions', () => { + cy.get('[data-attr=math-property-selector-0]').should('not.exist') + + cy.get('[data-attr=math-selector-0]').click() + cy.get('[data-attr=math-total-0]').should('be.visible') + + cy.get('[data-attr=math-node-hogql-expression-0]').click() + cy.get('[data-attr=math-hogql-select-0]').click() + cy.get('[data-attr=inline-hogql-editor]').click().clear().type('avg(1042) * 2048') + cy.contains('Update HogQL expression').click() + + cy.get('[data-attr=chart-filter]').click() + cy.contains('Table').click() + cy.contains('2,134,016').should('exist') + }) + it('Apply specific filter on default pageview event', () => { cy.get('[data-attr=trend-element-subject-0]').click() cy.get('[data-attr=taxonomic-filter-searchfield]').click().type('Pageview') diff --git a/ee/clickhouse/models/test/test_filters.py b/ee/clickhouse/models/test/test_filters.py index 12cd0b17e593b..1cb32f3a27e19 100644 --- a/ee/clickhouse/models/test/test_filters.py +++ b/ee/clickhouse/models/test/test_filters.py @@ -250,6 +250,7 @@ def test_simplify_entities(self): "type": "events", "id": "$pageview", "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "custom_name": None, @@ -275,6 +276,7 @@ def test_simplify_entities_with_group_math(self): "type": "events", "id": "$pageview", "math": "unique_group", + "math_hogql": None, "math_property": None, "math_group_type_index": 2, "custom_name": None, diff --git a/ee/clickhouse/queries/test/test_breakdown_props.py b/ee/clickhouse/queries/test/test_breakdown_props.py index 945faa3ff4e50..e0442cffadcb1 100644 --- a/ee/clickhouse/queries/test/test_breakdown_props.py +++ b/ee/clickhouse/queries/test/test_breakdown_props.py @@ -396,7 +396,7 @@ def test_breakdown_with_math_property_session(self): ], } ) - aggregate_operation, _, _ = process_math(filter.entities[0], self.team) + aggregate_operation, _, _ = process_math(filter.entities[0], self.team, filter=filter) result = get_breakdown_prop_values(filter, filter.entities[0], aggregate_operation, self.team) # test should come first, based on aggregate operation, even if absolute count of events for diff --git a/ee/clickhouse/queries/test/test_paths.py b/ee/clickhouse/queries/test/test_paths.py index 433be166d793f..31ff0c0580a65 100644 --- a/ee/clickhouse/queries/test/test_paths.py +++ b/ee/clickhouse/queries/test/test_paths.py @@ -131,6 +131,7 @@ def test_step_limit(self): self.assertEqual([p1.uuid], self._get_people_at_path(filter, "3_/3", "4_/4")) @snapshot_clickhouse_queries + @freeze_time("2023-05-23T11:00:00.000Z") def test_step_conversion_times(self): _create_person(team_id=self.team.pk, distinct_ids=["fake"]) @@ -728,6 +729,7 @@ def test_team_and_local_path_cleaning_rules(self): self.assertEqual(response, correct_response) @snapshot_clickhouse_queries + @freeze_time("2023-05-23T11:00:00.000Z") def test_path_cleaning_rules_with_wildcard_groups(self): _create_person(distinct_ids=[f"user_1"], team=self.team) _create_person(distinct_ids=[f"user_2"], team=self.team) @@ -1576,6 +1578,7 @@ def test_end(self): ) @snapshot_clickhouse_queries + @freeze_time("2023-05-23T11:00:00.000Z") def test_event_inclusion_exclusion_filters(self): # P1 for pageview event _create_person(team_id=self.team.pk, distinct_ids=["p1"]) @@ -1720,6 +1723,7 @@ def test_event_inclusion_exclusion_filters(self): ) @snapshot_clickhouse_queries + @freeze_time("2023-05-23T11:00:00.000Z") def test_event_exclusion_filters_with_wildcard_groups(self): # P1 for pageview event /2/bar/1/foo _create_person(team_id=self.team.pk, distinct_ids=["p1"]) @@ -1939,6 +1943,7 @@ def test_event_inclusion_exclusion_filters_across_single_person(self): ) @snapshot_clickhouse_queries + @freeze_time("2023-05-23T11:00:00.000Z") def test_respect_session_limits(self): _create_person(team_id=self.team.pk, distinct_ids=["fake"]) @@ -2241,6 +2246,7 @@ def should_query_list(filter) -> Tuple[bool, bool]: self.assertEqual(should_query_list(filter), (False, True)) @snapshot_clickhouse_queries + @freeze_time("2023-05-23T11:00:00.000Z") def test_wildcard_groups_across_people(self): # P1 for pageview event /2/bar/1/foo _create_person(team_id=self.team.pk, distinct_ids=["p1"]) @@ -2336,6 +2342,7 @@ def test_wildcard_groups_across_people(self): ) @snapshot_clickhouse_queries + @freeze_time("2023-05-23T11:00:00.000Z") def test_wildcard_groups_evil_input(self): evil_string = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!" # P1 for pageview event /2/bar/1/foo @@ -3117,8 +3124,6 @@ def test_person_on_events_v2(self): ], ) - # Note: not using `@snapshot_clickhouse_queries` here because the ordering of the session_ids in the recording - # query is not guaranteed, so adding it would lead to a flaky test. @freeze_time("2012-01-01T03:21:34.000Z") @snapshot_clickhouse_queries def test_recording(self): diff --git a/ee/clickhouse/views/test/test_clickhouse_trends.py b/ee/clickhouse/views/test/test_clickhouse_trends.py index ed02afc937058..be67ce627f4ed 100644 --- a/ee/clickhouse/views/test/test_clickhouse_trends.py +++ b/ee/clickhouse/views/test/test_clickhouse_trends.py @@ -272,6 +272,7 @@ def test_can_specify_number_of_smoothing_intervals(client: Client): "name": "$pageview", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": ANY, "properties": {}, diff --git a/ee/tasks/test/test_calculate_cohort.py b/ee/tasks/test/test_calculate_cohort.py index 652f9078b0b4e..eafa80b0f7332 100644 --- a/ee/tasks/test/test_calculate_cohort.py +++ b/ee/tasks/test/test_calculate_cohort.py @@ -59,6 +59,7 @@ def test_create_stickiness_cohort(self, _insert_cohort_from_insight_filter): "name": "$pageview", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -131,6 +132,7 @@ def test_create_trends_cohort(self, _insert_cohort_from_insight_filter): "order": 0, "name": "$pageview", "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -233,6 +235,7 @@ def test_create_trends_cohort_arg_test(self, _insert_cohort_from_insight_filter) "order": 0, "name": "$pageview", "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -300,6 +303,7 @@ def test_create_funnels_cohort(self, _insert_cohort_from_insight_filter): "type": "events", "order": 0, "properties": [], + "math_hogql": None, "math_property": None, }, { @@ -309,6 +313,7 @@ def test_create_funnels_cohort(self, _insert_cohort_from_insight_filter): "type": "events", "order": 1, "properties": [], + "math_hogql": None, "math_property": None, }, ] @@ -332,7 +337,7 @@ def test_create_funnels_cohort(self, _insert_cohort_from_insight_filter): cohort_id, { "insight": "FUNNELS", - "events": '[{"id": "$pageview", "math": null, "name": "$pageview", "type": "events", "order": 0, "properties": [], "math_property": null}, {"id": "$another_view", "math": null, "name": "$another_view", "type": "events", "order": 1, "properties": [], "math_property": null}]', + "events": '[{"id": "$pageview", "math": null, "name": "$pageview", "type": "events", "order": 0, "properties": [], "math_hogql": null, "math_property": null}, {"id": "$another_view", "math": null, "name": "$another_view", "type": "events", "order": 1, "properties": [], "math_hogql": null, "math_property": null}]', "display": "FunnelViz", "interval": "day", "layout": "horizontal", diff --git a/frontend/__snapshots__/components-hogqleditor--hog-ql-editor.png b/frontend/__snapshots__/components-hogqleditor--hog-ql-editor.png new file mode 100644 index 0000000000000..a3380950bb232 Binary files /dev/null and b/frontend/__snapshots__/components-hogqleditor--hog-ql-editor.png differ diff --git a/frontend/__snapshots__/components-hogqleditor--no-value-person-properties-disabled.png b/frontend/__snapshots__/components-hogqleditor--no-value-person-properties-disabled.png new file mode 100644 index 0000000000000..db2bdae0e6ec0 Binary files /dev/null and b/frontend/__snapshots__/components-hogqleditor--no-value-person-properties-disabled.png differ diff --git a/frontend/__snapshots__/components-hogqleditor--no-value.png b/frontend/__snapshots__/components-hogqleditor--no-value.png new file mode 100644 index 0000000000000..65f1bea955712 Binary files /dev/null and b/frontend/__snapshots__/components-hogqleditor--no-value.png differ diff --git a/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx b/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx index 8fedc7b3366ce..c028caf027a90 100644 --- a/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx +++ b/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx @@ -146,16 +146,23 @@ function SeriesDisplay({ {insightType !== InsightType.FUNNELS && (
counted by{' '} - {mathDefinition?.category === MathCategory.PropertyValue && filter.math_property && ( + {mathDefinition?.category === MathCategory.HogQLExpression ? ( + {filter.math_hogql} + ) : ( <> - {' '} - event's - - - + {mathDefinition?.category === MathCategory.PropertyValue && + filter.math_property && ( + <> + {' '} + event's + + + + + )} + {mathDefinition?.name.toLowerCase()} )} - {mathDefinition?.name.toLowerCase()}
)} {filter.properties && filter.properties.length > 0 && ( @@ -170,23 +177,21 @@ function SeriesDisplay({ } > - - {insightType === InsightType.FUNNELS ? 'Performed' : 'Showing'} - {filter.custom_name && "{filter.custom_name}"} - {filter.type === 'actions' && filter.id ? ( - - {filter.name} - - ) : ( - - - - )} - + {insightType === InsightType.FUNNELS ? 'Performed' : 'Showing'} + {filter.custom_name && "{filter.custom_name}"} + {filter.type === 'actions' && filter.id ? ( + + {filter.name} + + ) : ( + + + + )} ) } diff --git a/frontend/src/lib/components/HogQLEditor/HogQLEditor.stories.tsx b/frontend/src/lib/components/HogQLEditor/HogQLEditor.stories.tsx new file mode 100644 index 0000000000000..b924e3dc8d9a5 --- /dev/null +++ b/frontend/src/lib/components/HogQLEditor/HogQLEditor.stories.tsx @@ -0,0 +1,29 @@ +import { ComponentStory, Meta } from '@storybook/react' +import { HogQLEditor } from './HogQLEditor' +import { useState } from 'react' + +export default { + title: 'Components/HogQLEditor', + component: HogQLEditor, +} as Meta + +const Template: ComponentStory = (props): JSX.Element => { + const [value, onChange] = useState(props.value ?? "countIf(properties.$browser = 'Chrome')") + return +} + +export const HogQLEditor_ = Template.bind({}) +HogQLEditor_.args = {} + +export const NoValue = Template.bind({}) +NoValue.args = { + value: '', + disableAutoFocus: true, +} + +export const NoValuePersonPropertiesDisabled = Template.bind({}) +NoValuePersonPropertiesDisabled.args = { + disablePersonProperties: true, + value: '', + disableAutoFocus: true, +} diff --git a/frontend/src/lib/components/HogQLEditor/HogQLEditor.tsx b/frontend/src/lib/components/HogQLEditor/HogQLEditor.tsx new file mode 100644 index 0000000000000..66d6e43aab3f7 --- /dev/null +++ b/frontend/src/lib/components/HogQLEditor/HogQLEditor.tsx @@ -0,0 +1,78 @@ +import { useEffect, useState } from 'react' +import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' +import { CLICK_OUTSIDE_BLOCK_CLASS } from 'lib/hooks/useOutsideClickHandler' +import { LemonButton } from 'lib/lemon-ui/LemonButton' + +export interface HogQLEditorProps { + onChange: (value: string) => void + value: string | undefined + disablePersonProperties?: boolean + disableAutoFocus?: boolean + disableCmdEnter?: boolean + submitText?: string + placeholder?: string +} + +export function HogQLEditor({ + onChange, + value, + disablePersonProperties, + disableAutoFocus, + disableCmdEnter, + submitText, + placeholder, +}: HogQLEditorProps): JSX.Element { + const [localValue, setLocalValue] = useState(value || '') + useEffect(() => { + setLocalValue(value || '') + }, [value]) + + return ( + <> + setLocalValue(newValue)} + autoFocus={!disableAutoFocus} + onFocus={ + disableAutoFocus + ? undefined + : (e) => { + e.target.selectionStart = localValue.length // Focus at the end of the input + } + } + onPressCmdEnter={disableCmdEnter ? undefined : () => onChange(localValue)} + className={`font-mono ${CLICK_OUTSIDE_BLOCK_CLASS}`} + minRows={6} + maxRows={6} + placeholder={ + placeholder ?? + (disablePersonProperties + ? "Enter HogQL expression, such as:\n- properties.$current_url\n- toInt(properties.`Long Field Name`) * 10\n- concat(event, ' ', distinct_id)\n- if(1 < 2, 'small', 'large')" + : "Enter HogQL Expression, such as:\n- properties.$current_url\n- person.properties.$geoip_country_name\n- toInt(properties.`Long Field Name`) * 10\n- concat(event, ' ', distinct_id)\n- if(1 < 2, 'small', 'large')") + } + /> + onChange(localValue)} + disabledReason={!localValue ? 'Please enter a HogQL expression' : undefined} + center + > + {submitText ?? 'Update HogQL expression'} + +
+ {disablePersonProperties ? ( +
+ Note: person.properties can't be used here. +
+ ) : null} + +
+ + ) +} diff --git a/frontend/src/lib/components/InsightLabel/index.tsx b/frontend/src/lib/components/InsightLabel/index.tsx index 8cadeb9bac97e..bf09389014f24 100644 --- a/frontend/src/lib/components/InsightLabel/index.tsx +++ b/frontend/src/lib/components/InsightLabel/index.tsx @@ -45,10 +45,11 @@ interface InsightsLabelProps { interface MathTagProps { math: string | undefined mathProperty: string | undefined + mathHogQL: string | undefined mathGroupTypeIndex: number | null | undefined } -function MathTag({ math, mathProperty, mathGroupTypeIndex }: MathTagProps): JSX.Element { +function MathTag({ math, mathProperty, mathHogQL, mathGroupTypeIndex }: MathTagProps): JSX.Element { const { mathDefinitions } = useValues(mathsLogic) const { aggregationLabel } = useValues(groupsModel) @@ -74,6 +75,13 @@ function MathTag({ math, mathProperty, mathGroupTypeIndex }: MathTagProps): JSX. ) } + if (math === 'hogql') { + return ( + + {String(mathHogQL)} + + ) + } return {capitalizeFirstLetter(math)} } @@ -153,6 +161,7 @@ export function InsightLabel({ )} diff --git a/frontend/src/lib/components/TaxonomicFilter/InlineHogQLEditor.tsx b/frontend/src/lib/components/TaxonomicFilter/InlineHogQLEditor.tsx index d4a8d028681f3..320e04d706f03 100644 --- a/frontend/src/lib/components/TaxonomicFilter/InlineHogQLEditor.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/InlineHogQLEditor.tsx @@ -1,7 +1,5 @@ -import { useEffect, useState } from 'react' -import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { TaxonomicFilterValue } from 'lib/components/TaxonomicFilter/types' +import { HogQLEditor } from 'lib/components/HogQLEditor/HogQLEditor' export interface InlineHogQLEditorProps { value?: TaxonomicFilterValue @@ -9,42 +7,14 @@ export interface InlineHogQLEditorProps { } export function InlineHogQLEditor({ value, onChange }: InlineHogQLEditorProps): JSX.Element { - const [localValue, setLocalValue] = useState(value) - useEffect(() => { - setLocalValue(value) - }, [value]) return (
- setLocalValue(e)} - className="font-mono" - minRows={6} - maxRows={6} - placeholder={ - "Enter HogQL Expression, such as:\n- properties.$current_url\n- person.properties.$geoip_country_name\n- toInt(properties.`Long Field Name`) * 10\n- concat(event, ' ', distinct_id)\n- if(1 < 2, 'small', 'large')" - } - // :TRICKY: No autofocus here. It's controlled in the TaxonomicFilter. - // autoFocus + - { - onChange(String(localValue)) - setLocalValue('') - }} - disabled={!localValue} - center - > - {value ? 'Update HogQL expression' : 'Add HogQL expression'} - -
) } diff --git a/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.tsx b/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.tsx index 6adc482c53651..4e48d43e1c6a1 100644 --- a/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.tsx @@ -53,7 +53,7 @@ export function TaxonomicFilter({ } const logic = taxonomicFilterLogic(taxonomicFilterLogicProps) - const { searchQuery, searchPlaceholder } = useValues(logic) + const { searchQuery, searchPlaceholder, activeTab } = useValues(logic) const { setSearchQuery, moveUp, moveDown, tabLeft, tabRight, selectSelected } = useActions(logic) useEffect(() => { @@ -78,61 +78,63 @@ export function TaxonomicFilter({ // eslint-disable-next-line react/forbid-dom-props style={style} > -
- - You can easily navigate between tabs with your keyboard.{' '} -
- Use tab or right arrow to move to the next tab. -
-
- Use shift + tab or left arrow to move to the previous tab. -
- - } - > - - - } - onKeyDown={(e) => { - if (e.key === 'ArrowUp') { - e.preventDefault() - moveUp() - } - if (e.key === 'ArrowDown') { - e.preventDefault() - moveDown() + {activeTab !== TaxonomicFilterGroupType.HogQLExpression || taxonomicGroupTypes.length > 1 ? ( +
+ + You can easily navigate between tabs with your keyboard.{' '} +
+ Use tab or right arrow to move to the next tab. +
+
+ Use shift + tab or left arrow to move to the previous tab. +
+ + } + > + + } - if (e.key === 'Tab') { - e.preventDefault() - if (e.shiftKey) { - tabLeft() - } else { - tabRight() + onKeyDown={(e) => { + if (e.key === 'ArrowUp') { + e.preventDefault() + moveUp() + } + if (e.key === 'ArrowDown') { + e.preventDefault() + moveDown() + } + if (e.key === 'Tab') { + e.preventDefault() + if (e.shiftKey) { + tabLeft() + } else { + tabRight() + } } - } - if (e.key === 'Enter') { - e.preventDefault() - selectSelected() - } - if (e.key === 'Escape') { - e.preventDefault() - onClose?.() - } - }} - ref={searchInputRef} - onChange={(newValue) => setSearchQuery(newValue)} - /> -
+ if (e.key === 'Enter') { + e.preventDefault() + selectSelected() + } + if (e.key === 'Escape') { + e.preventDefault() + onClose?.() + } + }} + ref={searchInputRef} + onChange={(newValue) => setSearchQuery(newValue)} + /> +
+ ) : null} diff --git a/frontend/src/lib/utils.tsx b/frontend/src/lib/utils.tsx index e4721117f3227..ee982d46a0cd5 100644 --- a/frontend/src/lib/utils.tsx +++ b/frontend/src/lib/utils.tsx @@ -388,6 +388,8 @@ export function formatPropertyLabel( export function formatLabel(label: string, action: ActionFilter): string { if (action.math === 'dau') { label += ` (Unique users) ` + } else if (action.math === 'hogql') { + label += ` (${action.math_hogql})` } else if (['sum', 'avg', 'min', 'max', 'median', 'p90', 'p95', 'p99'].includes(action.math || '')) { label += ` (${action.math} of ${action.math_property}) ` } diff --git a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts index c52259d133d84..04b16b5d68417 100644 --- a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts +++ b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts @@ -54,6 +54,7 @@ export const actionsAndEventsToSeries = ({ properties: f.properties, math: f.math || 'total', math_property: f.math_property, + math_hogql: f.math_hogql, math_group_type_index: f.math_group_type_index, }) if (f.type === 'actions') { diff --git a/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.ts b/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.ts index 83a8c60a169ba..948dbcd18641f 100644 --- a/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.ts +++ b/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.ts @@ -31,6 +31,7 @@ export const seriesToActionsAndEvents = ( // TODO: math is not supported by funnel and lifecycle queries math: node.math, math_property: node.math_property, + math_hogql: node.math_hogql, math_group_type_index: node.math_group_type_index, properties: node.properties as any, // TODO, }) diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 3117b00cd275c..178ec17cb44db 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -35,6 +35,9 @@ }, { "$ref": "#/definitions/GroupMathType" + }, + { + "$ref": "#/definitions/HogQLMathType" } ] }, @@ -42,6 +45,9 @@ "enum": [0, 1, 2, 3, 4], "type": "number" }, + "math_hogql": { + "type": "string" + }, "math_property": { "type": "string" }, @@ -1606,6 +1612,9 @@ }, { "$ref": "#/definitions/GroupMathType" + }, + { + "$ref": "#/definitions/HogQLMathType" } ] }, @@ -1613,6 +1622,9 @@ "enum": [0, 1, 2, 3, 4], "type": "number" }, + "math_hogql": { + "type": "string" + }, "math_property": { "type": "string" }, @@ -2036,6 +2048,10 @@ "HogQLExpression": { "type": "string" }, + "HogQLMathType": { + "const": "hogql", + "type": "string" + }, "HogQLPropertyFilter": { "additionalProperties": false, "properties": { diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 0404421199489..ae0bfb584e796 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -19,6 +19,7 @@ import { StickinessFilterType, LifecycleFilterType, LifecycleToggle, + HogQLMathType, } from '~/types' /** @@ -124,8 +125,9 @@ export interface HogQLQuery extends DataNode { export interface EntityNode extends DataNode { name?: string custom_name?: string - math?: BaseMathType | PropertyMathType | CountPerActorMathType | GroupMathType + math?: BaseMathType | PropertyMathType | CountPerActorMathType | GroupMathType | HogQLMathType math_property?: string + math_hogql?: string math_group_type_index?: 0 | 1 | 2 | 3 | 4 /** Properties configurable in the interface */ properties?: AnyPropertyFilter[] diff --git a/frontend/src/queries/utils.ts b/frontend/src/queries/utils.ts index 2bb8bed7e25af..30424bece30d7 100644 --- a/frontend/src/queries/utils.ts +++ b/frontend/src/queries/utils.ts @@ -241,19 +241,6 @@ export function taxonomicFilterToHogQl( return null } -export function hogQlToTaxonomicFilter(hogQl: string): [TaxonomicFilterGroupType, TaxonomicFilterValue] { - if (hogQl.startsWith('person.properties.')) { - return [TaxonomicFilterGroupType.PersonProperties, hogQl.substring(18)] - } - if (hogQl.startsWith('properties.$feature/')) { - return [TaxonomicFilterGroupType.EventFeatureFlags, hogQl.substring(11)] - } - if (hogQl.startsWith('properties.')) { - return [TaxonomicFilterGroupType.EventProperties, hogQl.substring(11)] - } - return [TaxonomicFilterGroupType.HogQLExpression, hogQl] -} - export function isHogQlAggregation(hogQl: string): boolean { return ( hogQl.includes('count(') || diff --git a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx index 76cf26d7a7ef4..5d1a11ede2849 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx +++ b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx @@ -10,6 +10,7 @@ import { BaseMathType, PropertyMathType, CountPerActorMathType, + HogQLMathType, } from '~/types' import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' import { entityFilterLogic } from '../entityFilterLogic' @@ -36,6 +37,8 @@ import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonSelect, LemonSelectOption, LemonSelectOptions } from '@posthog/lemon-ui' import { useState } from 'react' import { GroupIntroductionFooter } from 'scenes/groups/GroupsIntroduction' +import { LemonDropdown } from 'lib/lemon-ui/LemonDropdown' +import { HogQLEditor } from 'lib/components/HogQLEditor/HogQLEditor' const DragHandle = sortableHandle(() => ( @@ -133,7 +136,12 @@ export function ActionFilterRow({ const propertyFiltersVisible = typeof filter.order === 'number' ? entityFilterVisible[filter.order] : false let name: string | null | undefined, value: PropertyFilterValue - const { math, math_property: mathProperty, math_group_type_index: mathGroupTypeIndex } = filter + const { + math, + math_property: mathProperty, + math_hogql: mathHogQL, + math_group_type_index: mathGroupTypeIndex, + } = filter const onClose = (): void => { removeLocalFilter({ ...filter, index }) @@ -145,6 +153,10 @@ export function ActionFilterRow({ mathDefinitions[selectedMath]?.category === MathCategory.PropertyValue ? mathProperty ?? '$time' : undefined, + math_hogql: + mathDefinitions[selectedMath]?.category === MathCategory.HogQLExpression + ? mathHogQL ?? 'count()' + : undefined, type: filter.type, index, }) @@ -152,11 +164,21 @@ export function ActionFilterRow({ const onMathPropertySelect = (_: unknown, property: string): void => { updateFilterMath({ ...filter, + math_hogql: undefined, math_property: property, index, }) } + const onMathHogQLSelect = (_: unknown, hogql: string): void => { + updateFilterMath({ + ...filter, + math_property: undefined, + math_hogql: hogql, + index, + }) + } + if (filter.type === EntityTypes.ACTIONS) { const action = actions.find((action) => action.id === filter.id) name = action?.name || filter.name @@ -331,6 +353,34 @@ export function ActionFilterRow({ /> )} + {mathDefinitions[math || BaseMathType.TotalCount]?.category === + MathCategory.HogQLExpression && ( +
+ + + onMathHogQLSelect(index, currentValue) + } + /> +
+ } + > + + {mathHogQL} + + + + )} )} @@ -472,6 +522,14 @@ function useMathSelectorOptions({ 'data-attr': `math-node-property-value-${index}`, }) } + + options.push({ + value: HogQLMathType.HogQL, + label: 'HogQL expression', + tooltip: 'Aggregate events by custom SQL expression.', + 'data-attr': `math-node-hogql-expression-${index}`, + }) + return [ { options, diff --git a/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts b/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts index 647db3e81f495..2a59fecc356d8 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts +++ b/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts @@ -60,6 +60,7 @@ export const entityFilterLogic = kea([ type: filter.type as EntityType, math: filter.math, math_property: filter.math_property, + math_hogql: filter.math_hogql, index: filter.index, math_group_type_index: filter.math_group_type_index, }), diff --git a/frontend/src/scenes/insights/filters/AggregationSelect.tsx b/frontend/src/scenes/insights/filters/AggregationSelect.tsx index 930629726f12c..5dcead0159f9c 100644 --- a/frontend/src/scenes/insights/filters/AggregationSelect.tsx +++ b/frontend/src/scenes/insights/filters/AggregationSelect.tsx @@ -1,6 +1,6 @@ import { useActions, useValues } from 'kea' import { groupsModel } from '~/models/groupsModel' -import { LemonButton, LemonSelect, LemonSelectSection, LemonTextArea } from '@posthog/lemon-ui' +import { LemonSelect, LemonSelectSection } from '@posthog/lemon-ui' import { groupsAccessLogic } from 'lib/introductions/groupsAccessLogic' import { GroupIntroductionFooter } from 'scenes/groups/GroupsIntroduction' import { funnelLogic } from 'scenes/funnels/funnelLogic' @@ -10,8 +10,7 @@ import { isFunnelsQuery, isInsightQueryNode } from '~/queries/utils' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' import { FunnelsQuery } from '~/queries/schema' import { isFunnelsFilter } from 'scenes/insights/sharedUtils' -import { useEffect, useState } from 'react' -import { CLICK_OUTSIDE_BLOCK_CLASS } from 'lib/hooks/useOutsideClickHandler' +import { HogQLEditor } from 'lib/components/HogQLEditor/HogQLEditor' type AggregationSelectProps = { insightProps: InsightLogicProps @@ -168,7 +167,18 @@ function AggregationSelectComponent({ value: !value || baseValues.includes(value) ? '' : value, label: {value}, CustomControl: function CustomHogQLOptionWrapped({ onSelect }) { - return + return ( +
+ +
+ ) }, }, ], @@ -189,49 +199,3 @@ function AggregationSelectComponent({ /> ) } - -function CustomHogQLOption({ - onSelect, - actualValue, -}: { - onSelect: (value: string) => void - actualValue: string | undefined -}): JSX.Element { - const [localValue, setLocalValue] = useState(actualValue || '') - useEffect(() => { - setLocalValue(actualValue || '') - }, [actualValue]) - - return ( -
- setLocalValue(newValue)} - onFocus={(e) => { - e.target.selectionStart = localValue.length // Focus at the end of the input - }} - onPressCmdEnter={() => onSelect(localValue)} - className={`font-mono ${CLICK_OUTSIDE_BLOCK_CLASS}`} - minRows={6} - maxRows={6} - autoFocus - placeholder={'Enter HogQL expression. Note: person property access is not enabled.'} - /> - onSelect(localValue)} - disabledReason={!localValue ? 'Please enter a HogQL expression' : undefined} - center - > - Aggregate by HogQL expression - - -
- ) -} diff --git a/frontend/src/scenes/insights/summarizeInsight.test.ts b/frontend/src/scenes/insights/summarizeInsight.test.ts index 7f88b78c853cf..eb110c3804006 100644 --- a/frontend/src/scenes/insights/summarizeInsight.test.ts +++ b/frontend/src/scenes/insights/summarizeInsight.test.ts @@ -15,6 +15,7 @@ import { import { BASE_MATH_DEFINITIONS, COUNT_PER_ACTOR_MATH_DEFINITIONS, + HOGQL_MATH_DEFINITIONS, MathCategory, MathDefinition, PROPERTY_MATH_DEFINITIONS, @@ -61,6 +62,7 @@ const mathDefinitions: Record = { }, ...PROPERTY_MATH_DEFINITIONS, ...COUNT_PER_ACTOR_MATH_DEFINITIONS, + ...HOGQL_MATH_DEFINITIONS, } const summaryContext: SummaryContext = { diff --git a/frontend/src/scenes/insights/summarizeInsight.ts b/frontend/src/scenes/insights/summarizeInsight.ts index 339c754a90e15..fba6014b00385 100644 --- a/frontend/src/scenes/insights/summarizeInsight.ts +++ b/frontend/src/scenes/insights/summarizeInsight.ts @@ -148,6 +148,8 @@ function summarizeInsightFilters(filters: AnyPartialFilterType, context: Summary ? 'unique groups' : mathType }` + } else if (mathDefinition?.category === MathCategory.HogQLExpression) { + series = localFilter.math_hogql ?? 'HogQL' } else { series = `${getDisplayNameFromEntityFilter(localFilter)} ${ mathDefinition diff --git a/frontend/src/scenes/trends/mathsLogic.tsx b/frontend/src/scenes/trends/mathsLogic.tsx index cd5262f66a497..e62d0a64b308f 100644 --- a/frontend/src/scenes/trends/mathsLogic.tsx +++ b/frontend/src/scenes/trends/mathsLogic.tsx @@ -1,7 +1,7 @@ import { kea } from 'kea' import { groupsModel } from '~/models/groupsModel' import type { mathsLogicType } from './mathsLogicType' -import { BaseMathType, CountPerActorMathType, PropertyMathType } from '~/types' +import { BaseMathType, CountPerActorMathType, HogQLMathType, PropertyMathType } from '~/types' import { groupsAccessLogic } from 'lib/introductions/groupsAccessLogic' export enum MathCategory { @@ -10,6 +10,7 @@ export enum MathCategory { ActorCount, EventCountPerActor, PropertyValue, + HogQLExpression, } export interface MathDefinition { @@ -199,6 +200,14 @@ export const PROPERTY_MATH_DEFINITIONS: Record category: MathCategory.PropertyValue, }, } +export const HOGQL_MATH_DEFINITIONS: Record = { + [HogQLMathType.HogQL]: { + name: 'HogQL expression', + shortName: 'HogQL expression', + description: <>Aggregate with a custom HogQL expression., + category: MathCategory.HogQLExpression, + }, +} export const COUNT_PER_ACTOR_MATH_DEFINITIONS: Record = { [CountPerActorMathType.Average]: { @@ -284,6 +293,7 @@ export const mathsLogic = kea({ ...groupsMathDefinitions, ...PROPERTY_MATH_DEFINITIONS, ...COUNT_PER_ACTOR_MATH_DEFINITIONS, + ...HOGQL_MATH_DEFINITIONS, } return allMathDefinitions }, diff --git a/frontend/src/types.ts b/frontend/src/types.ts index a98aad9d8830c..6adbec323afe3 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1782,6 +1782,7 @@ export interface ActionFilter extends EntityFilter { math?: string math_property?: string math_group_type_index?: number | null + math_hogql?: string properties?: AnyPropertyFilter[] type: EntityType } @@ -2575,6 +2576,9 @@ export enum CountPerActorMathType { P99 = 'p99_count_per_actor', } +export enum HogQLMathType { + HogQL = 'hogql', +} export enum GroupMathType { UniqueGroup = 'unique_group', } diff --git a/playwright/e2e-vrt/components/ActivityLog.spec.ts-snapshots/Activity-Log-displays-insight-activity-1-chromium-linux.png b/playwright/e2e-vrt/components/ActivityLog.spec.ts-snapshots/Activity-Log-displays-insight-activity-1-chromium-linux.png index d9011e3c6ec54..5edde0ae3f2b0 100644 Binary files a/playwright/e2e-vrt/components/ActivityLog.spec.ts-snapshots/Activity-Log-displays-insight-activity-1-chromium-linux.png and b/playwright/e2e-vrt/components/ActivityLog.spec.ts-snapshots/Activity-Log-displays-insight-activity-1-chromium-linux.png differ diff --git a/posthog/api/test/test_insight.py b/posthog/api/test/test_insight.py index ea4b3ed83e091..6d85b456e89c8 100644 --- a/posthog/api/test/test_insight.py +++ b/posthog/api/test/test_insight.py @@ -1084,6 +1084,7 @@ def test_save_new_funnel(self) -> None: "type": "events", "order": 0, "properties": [], + "math_hogql": None, "math_property": None, }, { @@ -1093,6 +1094,7 @@ def test_save_new_funnel(self) -> None: "type": "events", "order": 2, "properties": [], + "math_hogql": None, "math_property": None, }, ], diff --git a/posthog/models/entity/entity.py b/posthog/models/entity/entity.py index 29721fbfa1fac..3472938a313a1 100644 --- a/posthog/models/entity/entity.py +++ b/posthog/models/entity/entity.py @@ -37,6 +37,7 @@ "p90_count_per_actor", "p95_count_per_actor", "p99_count_per_actor", + "hogql", ] @@ -54,6 +55,7 @@ class Entity(PropertyMixin): custom_name: Optional[str] math: Optional[MathType] math_property: Optional[str] + math_hogql: Optional[str] math_group_type_index: Optional[GroupTypeIndex] # Index is not set at all by default (meaning: access = AttributeError) - it's populated in EntitiesMixin.entities # Used for identifying entities within a single query during query building, @@ -77,6 +79,7 @@ def __init__(self, data: Dict[str, Any]) -> None: self.custom_name = custom_name self.math = data.get("math") self.math_property = data.get("math_property") + self.math_hogql = data.get("math_hogql") self.math_group_type_index = validate_group_type_index( "math_group_type_index", data.get("math_group_type_index") ) @@ -96,6 +99,7 @@ def to_dict(self) -> Dict[str, Any]: "custom_name": self.custom_name, "math": self.math, "math_property": self.math_property, + "math_hogql": self.math_hogql, "math_group_type_index": self.math_group_type_index, "properties": self.property_groups.to_dict(), } @@ -145,7 +149,9 @@ def get_action(self) -> Action: except: raise ValidationError(f"Action ID {self.id} does not exist!") - __repr__ = sane_repr("id", "type", "order", "name", "custom_name", "math", "math_property", "properties") + __repr__ = sane_repr( + "id", "type", "order", "name", "custom_name", "math", "math_property", "math_hogql", "properties" + ) class ExclusionEntity(Entity, FunnelFromToStepsMixin): diff --git a/posthog/models/filters/test/test_retention_filter.py b/posthog/models/filters/test/test_retention_filter.py index ae70e9df3cfd2..b3e652f91afc0 100644 --- a/posthog/models/filters/test/test_retention_filter.py +++ b/posthog/models/filters/test/test_retention_filter.py @@ -23,6 +23,7 @@ def test_fill_date_from_and_date_to(self): "returning_entity": { "id": "$pageview", "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "name": "$pageview", @@ -34,6 +35,7 @@ def test_fill_date_from_and_date_to(self): "target_entity": { "id": "$pageview", "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "name": "$pageview", @@ -65,6 +67,7 @@ def test_fill_date_from_and_date_to(self): "returning_entity": { "id": "$pageview", "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "name": "$pageview", @@ -76,6 +79,7 @@ def test_fill_date_from_and_date_to(self): "target_entity": { "id": "$pageview", "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "name": "$pageview", diff --git a/posthog/models/filters/test/test_stickiness_filter.py b/posthog/models/filters/test/test_stickiness_filter.py index bd31464f1e6f6..b2b5d70bda46d 100644 --- a/posthog/models/filters/test/test_stickiness_filter.py +++ b/posthog/models/filters/test/test_stickiness_filter.py @@ -31,6 +31,7 @@ def test_filter_properties(self): "name": "$pageview", "custom_name": "Custom event", "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": {}, diff --git a/posthog/queries/test/__snapshots__/test_trends.ambr b/posthog/queries/test/__snapshots__/test_trends.ambr index edc6b7bf97a82..9775b6f62f1de 100644 --- a/posthog/queries/test/__snapshots__/test_trends.ambr +++ b/posthog/queries/test/__snapshots__/test_trends.ambr @@ -4796,6 +4796,32 @@ ORDER BY breakdown_value ' --- +# name: TestTrends.test_trends_with_hogql_math + ' + + SELECT groupArray(day_start) as date, + groupArray(count) AS total + FROM + (SELECT SUM(total) AS count, + day_start + FROM + (SELECT toUInt16(0) AS total, + toStartOfWeek(toDateTime('2020-01-04 23:59:59', 'UTC') - toIntervalWeek(number)) AS day_start + FROM numbers(dateDiff('week', toStartOfWeek(toDateTime('2019-12-28 00:00:00', 'UTC')), toDateTime('2020-01-04 23:59:59', 'UTC'))) + UNION ALL SELECT toUInt16(0) AS total, + toStartOfWeek(toDateTime('2019-12-28 00:00:00', 'UTC')) + UNION ALL SELECT plus(avg(toInt64OrNull(nullIf(nullIf(events.`$session_id`, ''), 'null'))), 1000) AS total, + toStartOfWeek(toTimeZone(toDateTime(timestamp, 'UTC'), 'UTC')) AS date + FROM events e + WHERE team_id = 2 + AND event = 'sign up' + AND toTimeZone(timestamp, 'UTC') >= toDateTime(toStartOfWeek(toDateTime('2019-12-28 00:00:00', 'UTC'), 0), 'UTC') + AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') + GROUP BY date) + GROUP BY day_start + ORDER BY day_start) + ' +--- # name: TestTrends.test_trends_with_session_property_single_aggregate_math ' diff --git a/posthog/queries/test/test_trends.py b/posthog/queries/test/test_trends.py index 9194d55562cf2..f33fc9dc85deb 100644 --- a/posthog/queries/test/test_trends.py +++ b/posthog/queries/test/test_trends.py @@ -1550,6 +1550,7 @@ def test_hour_interval(self): "name": "event_name", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -1604,6 +1605,7 @@ def test_day_interval(self): "name": "event_name", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -1648,6 +1650,7 @@ def test_week_interval(self): "name": "event_name", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -1676,6 +1679,7 @@ def test_month_interval(self): "name": "event_name", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -1704,6 +1708,7 @@ def test_interval_rounding(self): "name": "event_name", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -1732,6 +1737,7 @@ def test_interval_rounding_monthly(self): "name": "event_name", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -1759,6 +1765,7 @@ def test_today_timerange(self): "name": "event_name", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -1787,6 +1794,7 @@ def test_yesterday_timerange(self): "name": "event_name", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -1814,6 +1822,7 @@ def test_last24hours_timerange(self): "name": "event_name", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -1841,6 +1850,7 @@ def test_last48hours_timerange(self): "name": "event_name", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -1868,6 +1878,7 @@ def test_last7days_timerange(self): "name": "event_name", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -1920,6 +1931,7 @@ def test_last14days_timerange(self): "name": "event_name", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -1987,6 +1999,7 @@ def test_last30days_timerange(self): "name": "event_name", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -2032,6 +2045,7 @@ def test_last90days_timerange(self): "name": "event_name", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -2067,6 +2081,7 @@ def test_this_month_timerange(self): "name": "event_name", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -2103,6 +2118,7 @@ def test_previous_month_timerange(self): "name": "event_name", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -2138,6 +2154,7 @@ def test_year_to_date_timerange(self): "name": "event_name", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -2172,6 +2189,7 @@ def test_all_time_timerange(self): "name": "event_name", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -2205,6 +2223,7 @@ def test_custom_range_timerange(self): "name": "event_name", "custom_name": None, "math": None, + "math_hogql": None, "math_property": None, "math_group_type_index": None, "properties": [], @@ -2236,6 +2255,48 @@ def test_property_filtering(self): self.assertEqual(response[0]["labels"][5], "2-Jan-2020") self.assertEqual(response[0]["data"][5], 0) + @snapshot_clickhouse_queries + def test_trends_with_hogql_math(self): + _create_person( + team_id=self.team.pk, + distinct_ids=["blabla", "anonymous_id"], + properties={"$some_prop": "some_val", "number": 8}, + ) + _create_event( + team=self.team, + event="sign up", + distinct_id="blabla", + properties={"$session_id": 1}, + timestamp="2020-01-01 00:06:30", + ) + _create_event( + team=self.team, + event="sign up", + distinct_id="blabla", + properties={"$session_id": 5}, + timestamp="2020-01-02 00:06:45", + ) + + with freeze_time("2020-01-04T13:00:01Z"): + response = Trends().run( + Filter( + data={ + "interval": "week", + "events": [ + { + "id": "sign up", + "math": "hogql", + "math_hogql": "avg(toInt(properties.$session_id)) + 1000", + } + ], + }, + team=self.team, + ), + self.team, + ) + self.assertCountEqual(response[0]["labels"], ["22-Dec-2019", "29-Dec-2019"]) + self.assertCountEqual(response[0]["data"], [0, 1003]) + @snapshot_clickhouse_queries def test_trends_with_session_property_total_volume_math(self): _create_person( @@ -5329,7 +5390,7 @@ def test_timezones_hourly(self): "entity_math": "dau", "entity_type": "events", "events": '[{"id": "sign up", "type": "events", "order": null, "name": "sign ' - 'up", "custom_name": null, "math": "dau", "math_property": null, ' + 'up", "custom_name": null, "math": "dau", "math_property": null, "math_hogql": null, ' '"math_group_type_index": null, "properties": {}}]', "insight": "TRENDS", "interval": "hour", diff --git a/posthog/queries/trends/breakdown.py b/posthog/queries/trends/breakdown.py index 511cc2e480380..05b3a3360d695 100644 --- a/posthog/queries/trends/breakdown.py +++ b/posthog/queries/trends/breakdown.py @@ -140,6 +140,7 @@ def get_query(self) -> Tuple[str, Dict, Callable]: aggregate_operation, _, math_params = process_math( self.entity, self.team, + filter=self.filter, event_table_alias=self.EVENT_TABLE_ALIAS, person_id_alias=f"person_id" if self.person_on_events_mode == PersonOnEventsMode.V1_ENABLED diff --git a/posthog/queries/trends/total_volume.py b/posthog/queries/trends/total_volume.py index 7491b5a8b1fb1..64078ee5ab832 100644 --- a/posthog/queries/trends/total_volume.py +++ b/posthog/queries/trends/total_volume.py @@ -60,7 +60,11 @@ def _total_volume_query(self, entity: Entity, filter: Filter, team: Team) -> Tup person_id_alias = "person_id" aggregate_operation, join_condition, math_params = process_math( - entity, team, event_table_alias=TrendsEventQuery.EVENT_TABLE_ALIAS, person_id_alias=person_id_alias + entity, + team, + filter=filter, + event_table_alias=TrendsEventQuery.EVENT_TABLE_ALIAS, + person_id_alias=person_id_alias, ) trend_event_query = TrendsEventQuery( @@ -109,7 +113,10 @@ def _total_volume_query(self, entity: Entity, filter: Filter, team: Team) -> Tup aggregator=determine_aggregator(entity, team), ) else: - tag_queries(trend_volume_type="volume_aggregate") + if entity.math == "hogql": + tag_queries(trend_volume_type="hogql") + else: + tag_queries(trend_volume_type="volume_aggregate") content_sql = VOLUME_AGGREGATE_SQL.format(event_query_base=event_query_base, **content_sql_params) return (content_sql, params, self._parse_aggregate_volume_result(filter, entity, team.id)) @@ -158,7 +165,10 @@ def _total_volume_query(self, entity: Entity, filter: Filter, team: Team) -> Tup **content_sql_params, ) else: - tag_queries(trend_volume_type="volume") + if entity.math == "hogql": + tag_queries(trend_volume_type="hogql") + else: + tag_queries(trend_volume_type="volume") content_sql = VOLUME_SQL.format( timestamp_column="timestamp", event_query_base=event_query_base, diff --git a/posthog/queries/trends/trends_actors.py b/posthog/queries/trends/trends_actors.py index 9f65cdd321e40..a353618ebbbb8 100644 --- a/posthog/queries/trends/trends_actors.py +++ b/posthog/queries/trends/trends_actors.py @@ -156,6 +156,8 @@ def _aggregation_actor_field(self) -> str: @cached_property def _aggregation_actor_value_expression_with_params(self) -> Tuple[str, Dict[str, Any]]: if self.entity.math in PROPERTY_MATH_FUNCTIONS: - math_aggregate_operation, _, math_params = process_math(self.entity, self._team, event_table_alias="e") + math_aggregate_operation, _, math_params = process_math( + self.entity, self._team, filter=self._filter, event_table_alias="e" + ) return math_aggregate_operation, math_params return "count()", {} diff --git a/posthog/queries/trends/util.py b/posthog/queries/trends/util.py index e13fac05a55c8..3f87d70c94291 100644 --- a/posthog/queries/trends/util.py +++ b/posthog/queries/trends/util.py @@ -10,6 +10,7 @@ from sentry_sdk import capture_exception, push_scope from posthog.constants import MONTHLY_ACTIVE, NON_TIME_SERIES_DISPLAY_TYPES, UNIQUE_GROUPS, UNIQUE_USERS, WEEKLY_ACTIVE +from posthog.hogql.hogql import translate_hogql from posthog.models.entity import Entity from posthog.models.event.sql import EVENT_JOIN_PERSON_SQL from posthog.models.filters import Filter @@ -47,7 +48,11 @@ def process_math( - entity: Entity, team: Team, event_table_alias: Optional[str] = None, person_id_alias: str = "person_id" + entity: Entity, + team: Team, + filter: Filter, + event_table_alias: Optional[str] = None, + person_id_alias: str = "person_id", ) -> Tuple[str, str, Dict[str, Any]]: aggregate_operation = "count(*)" join_condition = "" @@ -80,6 +85,8 @@ def process_math( params[key] = entity.math_property elif entity.math in COUNT_PER_ACTOR_MATH_FUNCTIONS: aggregate_operation = f"{COUNT_PER_ACTOR_MATH_FUNCTIONS[entity.math]}(intermediate_count)" + elif entity.math == "hogql": + aggregate_operation = translate_hogql(entity.math_hogql, filter.hogql_context) return aggregate_operation, join_condition, params diff --git a/posthog/schema.py b/posthog/schema.py index 4fca24978f2de..829661e6f7d8a 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -649,8 +649,9 @@ class Config: ) kind: str = Field("EventsNode", const=True) limit: Optional[float] = None - math: Optional[Union[BaseMathType, PropertyMathType, CountPerActorMathType, str]] = None + math: Optional[Union[BaseMathType, PropertyMathType, CountPerActorMathType, str, str]] = None math_group_type_index: Optional[MathGroupTypeIndex1] = None + math_hogql: Optional[str] = None math_property: Optional[str] = None name: Optional[str] = None orderBy: Optional[List[str]] = Field(None, description="Columns to order by") @@ -824,8 +825,9 @@ class Config: ) id: float kind: str = Field("ActionsNode", const=True) - math: Optional[Union[BaseMathType, PropertyMathType, CountPerActorMathType, str]] = None + math: Optional[Union[BaseMathType, PropertyMathType, CountPerActorMathType, str, str]] = None math_group_type_index: Optional[MathGroupTypeIndex] = None + math_hogql: Optional[str] = None math_property: Optional[str] = None name: Optional[str] = None properties: Optional[