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[