From be184b65c37498c31457880d9c46abdf6ca49b1b Mon Sep 17 00:00:00 2001 From: Robbie Date: Wed, 8 Nov 2023 17:07:45 +0000 Subject: [PATCH] feat(web-analytics): Use person properties for web analytics source (#18423) * Use person properties for web analytics source * Help kea typegen out * Fix query runner types * Remove pointless comment --- .../lib/components/PropertyFilters/utils.ts | 5 + .../TaxonomicFilter/taxonomicFilterLogic.tsx | 4 + frontend/src/queries/schema.json | 19 +-- frontend/src/queries/schema.ts | 6 +- .../web-analytics/WebAnalyticsDataTable.tsx | 39 +++--- .../src/scenes/web-analytics/WebDashboard.tsx | 35 +++-- .../scenes/web-analytics/webAnalyticsLogic.ts | 124 +++++++----------- .../web_analytics/stats_table.py | 46 ++----- .../web_analytics_query_runner.py | 5 +- posthog/schema.py | 8 +- 10 files changed, 130 insertions(+), 161 deletions(-) diff --git a/frontend/src/lib/components/PropertyFilters/utils.ts b/frontend/src/lib/components/PropertyFilters/utils.ts index 9f5b93fa7c313..b6371b8881c31 100644 --- a/frontend/src/lib/components/PropertyFilters/utils.ts +++ b/frontend/src/lib/components/PropertyFilters/utils.ts @@ -79,6 +79,11 @@ export function isEventPropertyFilter(filter?: AnyFilterLike | null): filter is export function isPersonPropertyFilter(filter?: AnyFilterLike | null): filter is PersonPropertyFilter { return filter?.type === PropertyFilterType.Person } +export function isEventPropertyOrPersonPropertyFilter( + filter?: AnyFilterLike | null +): filter is EventPropertyFilter | PersonPropertyFilter { + return filter?.type === PropertyFilterType.Event || filter?.type === PropertyFilterType.Person +} export function isElementPropertyFilter(filter?: AnyFilterLike | null): filter is ElementPropertyFilter { return filter?.type === PropertyFilterType.Element } diff --git a/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx b/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx index 146e318549886..7bcd6833e25ab 100644 --- a/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx @@ -280,9 +280,13 @@ export const taxonomicFilterLogic = kea([ type: TaxonomicFilterGroupType.PersonProperties, endpoint: combineUrl(`api/projects/${teamId}/property_definitions`, { type: 'person', + properties: propertyAllowList?.[TaxonomicFilterGroupType.PersonProperties] + ? propertyAllowList[TaxonomicFilterGroupType.PersonProperties].join(',') + : undefined, }).url, getName: (personProperty: PersonProperty) => personProperty.name, getValue: (personProperty: PersonProperty) => personProperty.name, + propertyAllowList: propertyAllowList?.[TaxonomicFilterGroupType.PersonProperties], ...propertyTaxonomicGroupProps(true), }, { diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index fe6d72056e17a..190898ed03a60 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -3064,16 +3064,19 @@ "required": ["results"], "type": "object" }, + "WebAnalyticsPropertyFilter": { + "anyOf": [ + { + "$ref": "#/definitions/EventPropertyFilter" + }, + { + "$ref": "#/definitions/PersonPropertyFilter" + } + ] + }, "WebAnalyticsPropertyFilters": { "items": { - "anyOf": [ - { - "$ref": "#/definitions/EventPropertyFilter" - }, - { - "$ref": "#/definitions/HogQLPropertyFilter" - } - ] + "$ref": "#/definitions/WebAnalyticsPropertyFilter" }, "type": "array" }, diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index e3c0e792d11af..fcd957f9adf55 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -11,12 +11,12 @@ import { FunnelsFilterType, GroupMathType, HogQLMathType, - HogQLPropertyFilter, InsightShortId, IntervalType, LifecycleFilterType, LifecycleToggle, PathsFilterType, + PersonPropertyFilter, PropertyGroupFilter, PropertyMathType, RetentionFilterType, @@ -575,8 +575,8 @@ export interface SessionsTimelineQuery extends DataNode { before?: string response?: SessionsTimelineQueryResponse } - -export type WebAnalyticsPropertyFilters = (EventPropertyFilter | HogQLPropertyFilter)[] +export type WebAnalyticsPropertyFilter = EventPropertyFilter | PersonPropertyFilter +export type WebAnalyticsPropertyFilters = WebAnalyticsPropertyFilter[] export interface WebAnalyticsQueryBase { dateRange?: DateRange diff --git a/frontend/src/scenes/web-analytics/WebAnalyticsDataTable.tsx b/frontend/src/scenes/web-analytics/WebAnalyticsDataTable.tsx index 82263ae63d009..2354af9ce805d 100644 --- a/frontend/src/scenes/web-analytics/WebAnalyticsDataTable.tsx +++ b/frontend/src/scenes/web-analytics/WebAnalyticsDataTable.tsx @@ -6,6 +6,7 @@ import { webAnalyticsLogic } from 'scenes/web-analytics/webAnalyticsLogic' import { useCallback, useMemo } from 'react' import { Query } from '~/queries/Query/Query' import { countryCodeToFlag, countryCodeToName } from 'scenes/insights/views/WorldMap' +import { PropertyFilterType } from '~/types' const PercentageCell: QueryContextColumnComponent = ({ value }) => { if (typeof value === 'number') { @@ -109,36 +110,38 @@ const BreakdownValueCell: QueryContextColumnComponent = (props) => { } } -export const webStatsBreakdownToPropertyName = (breakdownBy: WebStatsBreakdown): string => { +export const webStatsBreakdownToPropertyName = ( + breakdownBy: WebStatsBreakdown +): { key: string; type: PropertyFilterType.Person | PropertyFilterType.Event } => { switch (breakdownBy) { case WebStatsBreakdown.Page: - return '$pathname' + return { key: '$pathname', type: PropertyFilterType.Event } case WebStatsBreakdown.InitialPage: - return '$client_session_initial_pathname' + return { key: '$initial_pathname', type: PropertyFilterType.Person } case WebStatsBreakdown.InitialReferringDomain: - return '$client_session_initial_referring_host' + return { key: '$initial_referring_domain', type: PropertyFilterType.Person } case WebStatsBreakdown.InitialUTMSource: - return '$client_session_initial_utm_source' + return { key: '$initial_utm_source', type: PropertyFilterType.Person } case WebStatsBreakdown.InitialUTMCampaign: - return '$client_session_initial_utm_campaign' + return { key: '$initial_utm_campaign', type: PropertyFilterType.Person } case WebStatsBreakdown.InitialUTMMedium: - return '$client_session_initial_utm_medium' + return { key: '$initial_utm_medium', type: PropertyFilterType.Person } case WebStatsBreakdown.InitialUTMContent: - return '$client_session_initial_utm_content' + return { key: '$initial_utm_content', type: PropertyFilterType.Person } case WebStatsBreakdown.InitialUTMTerm: - return '$client_session_initial_utm_term' + return { key: '$initial_utm_term', type: PropertyFilterType.Person } case WebStatsBreakdown.Browser: - return '$browser' + return { key: '$browser', type: PropertyFilterType.Event } case WebStatsBreakdown.OS: - return '$os' + return { key: '$os', type: PropertyFilterType.Event } case WebStatsBreakdown.DeviceType: - return '$device_type' + return { key: '$device_type', type: PropertyFilterType.Event } case WebStatsBreakdown.Country: - return '$geoip_country_code' + return { key: '$geoip_country_code', type: PropertyFilterType.Event } case WebStatsBreakdown.Region: - return '$geoip_subdivision_1_code' + return { key: '$geoip_subdivision_1_code', type: PropertyFilterType.Event } case WebStatsBreakdown.City: - return '$geoip_city_name' + return { key: '$geoip_city_name', type: PropertyFilterType.Event } default: throw new UnexpectedNeverError(breakdownBy) } @@ -176,13 +179,13 @@ export const WebStatsTableTile = ({ breakdownBy: WebStatsBreakdown }): JSX.Element => { const { togglePropertyFilter } = useActions(webAnalyticsLogic) - const propertyName = webStatsBreakdownToPropertyName(breakdownBy) + const { key, type } = webStatsBreakdownToPropertyName(breakdownBy) const onClick = useCallback( (breakdownValue: string) => { - togglePropertyFilter(propertyName, breakdownValue) + togglePropertyFilter(type, key, breakdownValue) }, - [togglePropertyFilter, propertyName] + [togglePropertyFilter, type, key] ) const context = useMemo((): QueryContext => { diff --git a/frontend/src/scenes/web-analytics/WebDashboard.tsx b/frontend/src/scenes/web-analytics/WebDashboard.tsx index 6838e252b5ebf..2f32fd815ec48 100644 --- a/frontend/src/scenes/web-analytics/WebDashboard.tsx +++ b/frontend/src/scenes/web-analytics/WebDashboard.tsx @@ -2,7 +2,7 @@ import { Query } from '~/queries/Query/Query' import { useActions, useValues } from 'kea' import { TabsTile, webAnalyticsLogic } from 'scenes/web-analytics/webAnalyticsLogic' import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' -import { isEventPropertyFilter } from 'lib/components/PropertyFilters/utils' +import { isEventPropertyOrPersonPropertyFilter } from 'lib/components/PropertyFilters/utils' import { NodeKind, QuerySchema } from '~/queries/schema' import { DateFilter } from 'lib/components/DateFilter/DateFilter' import { WebAnalyticsNotice } from 'scenes/web-analytics/WebAnalyticsNotice' @@ -18,8 +18,13 @@ const Filters = (): JSX.Element => {
setWebAnalyticsFilters(filters.filter(isEventPropertyFilter))} + taxonomicGroupTypes={[ + TaxonomicFilterGroupType.EventProperties, + TaxonomicFilterGroupType.PersonProperties, + ]} + onChange={(filters) => + setWebAnalyticsFilters(filters.filter(isEventPropertyOrPersonPropertyFilter)) + } propertyFilters={webAnalyticsFilters} pageKey={'web-analytics'} eventNames={['$pageview', '$pageleave', '$autocapture']} @@ -33,13 +38,23 @@ const Filters = (): JSX.Element => { '$geoip_country_code', '$geoip_subdivision_1_code', '$geoip_city_name', - '$client_session_initial_pathname', - '$client_session_initial_referring_host', - '$client_session_initial_utm_source', - '$client_session_initial_utm_campaign', - '$client_session_initial_utm_medium', - '$client_session_initial_utm_content', - '$client_session_initial_utm_term', + // re-enable after https://github.com/PostHog/posthog-js/pull/875 is merged + // '$client_session_initial_pathname', + // '$client_session_initial_referring_host', + // '$client_session_initial_utm_source', + // '$client_session_initial_utm_campaign', + // '$client_session_initial_utm_medium', + // '$client_session_initial_utm_content', + // '$client_session_initial_utm_term', + ], + [TaxonomicFilterGroupType.PersonProperties]: [ + '$initial_pathname', + '$initial_referring_domain', + '$initial_utm_source', + '$initial_utm_campaign', + '$initial_utm_medium', + '$initial_utm_content', + '$initial_utm_term', ], }} /> diff --git a/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts b/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts index dcf7b16b4d45c..fcf6e11dbcdf9 100644 --- a/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts +++ b/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts @@ -1,15 +1,14 @@ import { actions, connect, kea, listeners, path, reducers, selectors, sharedListeners } from 'kea' import type { webAnalyticsLogicType } from './webAnalyticsLogicType' -import { NodeKind, QuerySchema, WebAnalyticsPropertyFilters, WebStatsBreakdown } from '~/queries/schema' import { - BaseMathType, - ChartDisplayType, - EventPropertyFilter, - HogQLPropertyFilter, - PropertyFilterType, - PropertyOperator, -} from '~/types' + NodeKind, + QuerySchema, + WebAnalyticsPropertyFilter, + WebAnalyticsPropertyFilters, + WebStatsBreakdown, +} from '~/queries/schema' +import { BaseMathType, ChartDisplayType, PropertyFilterType, PropertyOperator } from '~/types' import { isNotNil } from 'lib/utils' export interface WebTileLayout { @@ -65,17 +64,20 @@ export enum GeographyTab { export const initialWebAnalyticsFilter = [] as WebAnalyticsPropertyFilters -const setOncePropertyNames = ['$initial_pathname', '$initial_referrer', '$initial_utm_source', '$initial_utm_campaign'] -const hogqlForSetOnceProperty = (key: string, value: string): string => `properties.$set_once.${key} = '${value}'` -const isHogqlForSetOnceProperty = (key: string, p: HogQLPropertyFilter): boolean => - setOncePropertyNames.includes(key) && p.key.startsWith(`properties.$set_once.${key} = `) - export const webAnalyticsLogic = kea([ path(['scenes', 'webAnalytics', 'webAnalyticsSceneLogic']), connect({}), actions({ setWebAnalyticsFilters: (webAnalyticsFilters: WebAnalyticsPropertyFilters) => ({ webAnalyticsFilters }), - togglePropertyFilter: (key: string, value: string) => ({ key, value }), + togglePropertyFilter: ( + type: PropertyFilterType.Event | PropertyFilterType.Person, + key: string, + value: string | number + ) => ({ + type, + key, + value, + }), setSourceTab: (tab: string) => ({ tab, }), @@ -93,80 +95,44 @@ export const webAnalyticsLogic = kea([ initialWebAnalyticsFilter, { setWebAnalyticsFilters: (_, { webAnalyticsFilters }) => webAnalyticsFilters, - togglePropertyFilter: (oldPropertyFilters, { key, value }) => { - if ( - oldPropertyFilters.some( - (f) => - (f.type === PropertyFilterType.Event && - f.key === key && - f.operator === PropertyOperator.Exact) || - (f.type === PropertyFilterType.HogQL && isHogqlForSetOnceProperty(key, f)) - ) - ) { + togglePropertyFilter: (oldPropertyFilters, { key, value, type }): WebAnalyticsPropertyFilters => { + const similarFilterExists = oldPropertyFilters.some( + (f) => f.type === type && f.key === key && f.operator === PropertyOperator.Exact + ) + if (similarFilterExists) { + // if there's already a matching property, turn it off or merge them return oldPropertyFilters .map((f) => { - if (setOncePropertyNames.includes(key)) { - if (f.type !== PropertyFilterType.HogQL) { - return f - } - if (!isHogqlForSetOnceProperty(key, f)) { - return f - } - // With the hogql properties, we don't even attempt to handle arrays, to avoiding - // needing a parser on the front end. Instead the logic is much simpler - const hogql = hogqlForSetOnceProperty(key, value) - if (f.key === hogql) { - return null + if (f.key !== key || f.type !== type || f.operator !== PropertyOperator.Exact) { + return f + } + const oldValue = (Array.isArray(f.value) ? f.value : [f.value]).filter(isNotNil) + let newValue: (string | number)[] + if (oldValue.includes(value)) { + // If there are multiple values for this filter, reduce that to just the one being clicked + if (oldValue.length > 1) { + newValue = [value] } else { - return { - type: PropertyFilterType.HogQL, - key, - value: hogql, - } as const + return null } } else { - if ( - f.key !== key || - f.type !== PropertyFilterType.Event || - f.operator !== PropertyOperator.Exact - ) { - return f - } - const oldValue = (Array.isArray(f.value) ? f.value : [f.value]).filter(isNotNil) - let newValue: (string | number)[] - if (oldValue.includes(value)) { - // If there are multiple values for this filter, reduce that to just the one being clicked - if (oldValue.length > 1) { - newValue = [value] - } else { - return null - } - } else { - newValue = [...oldValue, value] - } - return { - type: PropertyFilterType.Event, - key, - operator: PropertyOperator.Exact, - value: newValue, - } as const + newValue = [...oldValue, value] } + return { + type: PropertyFilterType.Event, + key, + operator: PropertyOperator.Exact, + value: newValue, + } as const }) .filter(isNotNil) } else { - let newFilter: EventPropertyFilter | HogQLPropertyFilter - if (setOncePropertyNames.includes(key)) { - newFilter = { - type: PropertyFilterType.HogQL, - key: hogqlForSetOnceProperty(key, value), - } - } else { - newFilter = { - type: PropertyFilterType.Event, - key, - value, - operator: PropertyOperator.Exact, - } + // no matching property, so add one + const newFilter: WebAnalyticsPropertyFilter = { + type, + key, + value, + operator: PropertyOperator.Exact, } return [...oldPropertyFilters, newFilter] diff --git a/posthog/hogql_queries/web_analytics/stats_table.py b/posthog/hogql_queries/web_analytics/stats_table.py index d3f8960afed99..0e3f9b67a1943 100644 --- a/posthog/hogql_queries/web_analytics/stats_table.py +++ b/posthog/hogql_queries/web_analytics/stats_table.py @@ -92,19 +92,19 @@ def counts_breakdown(self): case WebStatsBreakdown.Page: return ast.Field(chain=["properties", "$pathname"]) case WebStatsBreakdown.InitialPage: - return ast.Field(chain=["properties", "$client_session_initial_pathname"]) + return ast.Field(chain=["person", "properties", "$initial_pathname"]) case WebStatsBreakdown.InitialReferringDomain: - return ast.Field(chain=["properties", "$client_session_initial_referring_domain"]) + return ast.Field(chain=["person", "properties", "$initial_referring_domain"]) case WebStatsBreakdown.InitialUTMSource: - return ast.Field(chain=["properties", "$client_session_initial_utm_source"]) + return ast.Field(chain=["person", "properties", "$initial_utm_source"]) case WebStatsBreakdown.InitialUTMCampaign: - return ast.Field(chain=["properties", "$client_session_initial_utm_campaign"]) + return ast.Field(chain=["person", "properties", "$initial_utm_campaign"]) case WebStatsBreakdown.InitialUTMMedium: - return ast.Field(chain=["properties", "$client_session_initial_utm_medium"]) + return ast.Field(chain=["person", "properties", "$initial_utm_medium"]) case WebStatsBreakdown.InitialUTMTerm: - return ast.Field(chain=["properties", "$client_session_initial_utm_term"]) + return ast.Field(chain=["person", "properties", "$initial_utm_term"]) case WebStatsBreakdown.InitialUTMContent: - return ast.Field(chain=["properties", "$client_session_initial_utm_content"]) + return ast.Field(chain=["person", "properties", "$initial_utm_content"]) case WebStatsBreakdown.Browser: return ast.Field(chain=["properties", "$browser"]) case WebStatsBreakdown.OS: @@ -126,37 +126,9 @@ def bounce_breakdown(self): match self.query.breakdownBy: case WebStatsBreakdown.Page: # use initial pathname for bounce rate - return ast.Call(name="any", args=[ast.Field(chain=["properties", "$initial_pathname"])]) - case WebStatsBreakdown.InitialPage: - return ast.Call(name="any", args=[ast.Field(chain=["properties", "$initial_pathname"])]) - case WebStatsBreakdown.InitialReferringDomain: - return ast.Call(name="any", args=[ast.Field(chain=["properties", "$initial_referring_domain"])]) - case WebStatsBreakdown.InitialUTMSource: - return ast.Call(name="any", args=[ast.Field(chain=["properties", "$initial_utm_source"])]) - case WebStatsBreakdown.InitialUTMCampaign: - return ast.Call(name="any", args=[ast.Field(chain=["properties", "$initial_utm_campaign"])]) - case WebStatsBreakdown.InitialUTMMedium: - return ast.Call(name="any", args=[ast.Field(chain=["properties", "$initial_utm_medium"])]) - case WebStatsBreakdown.InitialUTMTerm: - return ast.Call(name="any", args=[ast.Field(chain=["properties", "$initial_utm_term"])]) - case WebStatsBreakdown.InitialUTMContent: - return ast.Call(name="any", args=[ast.Field(chain=["properties", "$initial_utm_content"])]) - case WebStatsBreakdown.Browser: - return ast.Call(name="any", args=[ast.Field(chain=["properties", "$browser"])]) - case WebStatsBreakdown.OS: - return ast.Call(name="any", args=[ast.Field(chain=["properties", "$os"])]) - case WebStatsBreakdown.DeviceType: - return ast.Call(name="any", args=[ast.Field(chain=["properties", "$device_type"])]) - case WebStatsBreakdown.Country: - return ast.Call(name="any", args=[ast.Field(chain=["properties", "$geoip_country_code"])]) - case WebStatsBreakdown.Region: - return parse_expr( - "any(tuple(properties.$geoip_country_code, properties.$geoip_subdivision_1_code, properties.$geoip_subdivision_1_name))" - ) - case WebStatsBreakdown.City: - return parse_expr("any(tuple(properties.$geoip_country_code, properties.$geoip_city_name))") + return ast.Call(name="any", args=[ast.Field(chain=["person", "properties", "$initial_pathname"])]) case _: - raise NotImplementedError("Breakdown not implemented") + return ast.Call(name="any", args=[self.counts_breakdown()]) def where_breakdown(self): match self.query.breakdownBy: diff --git a/posthog/hogql_queries/web_analytics/web_analytics_query_runner.py b/posthog/hogql_queries/web_analytics/web_analytics_query_runner.py index ccd2ad773e7e1..8e8d34d443f62 100644 --- a/posthog/hogql_queries/web_analytics/web_analytics_query_runner.py +++ b/posthog/hogql_queries/web_analytics/web_analytics_query_runner.py @@ -4,6 +4,7 @@ from typing import Optional, List, Union, Type from django.utils.timezone import datetime + from posthog.caching.insights_api import BASE_MINIMUM_INSIGHT_REFRESH_INTERVAL, REDUCED_MINIMUM_INSIGHT_REFRESH_INTERVAL from posthog.caching.utils import is_stale from posthog.hogql.parser import parse_expr @@ -16,7 +17,7 @@ WebTopClicksQuery, WebOverviewQuery, WebStatsTableQuery, - HogQLPropertyFilter, + PersonPropertyFilter, ) WebQueryNode = Union[ @@ -47,7 +48,7 @@ def pathname_property_filter(self) -> Optional[EventPropertyFilter]: return None @cached_property - def property_filters_without_pathname(self) -> List[Union[EventPropertyFilter, HogQLPropertyFilter]]: + def property_filters_without_pathname(self) -> List[Union[EventPropertyFilter, PersonPropertyFilter]]: return [p for p in self.query.properties if p.key != "$pathname"] def session_where(self, include_previous_period: Optional[bool] = None): diff --git a/posthog/schema.py b/posthog/schema.py index a219fcdcc3b38..a73c5f22c0e45 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -950,7 +950,7 @@ class WebAnalyticsQueryBase(BaseModel): extra="forbid", ) dateRange: Optional[DateRange] = None - properties: List[Union[EventPropertyFilter, HogQLPropertyFilter]] + properties: List[Union[EventPropertyFilter, PersonPropertyFilter]] class WebOverviewQuery(BaseModel): @@ -959,7 +959,7 @@ class WebOverviewQuery(BaseModel): ) dateRange: Optional[DateRange] = None kind: Literal["WebOverviewQuery"] = "WebOverviewQuery" - properties: List[Union[EventPropertyFilter, HogQLPropertyFilter]] + properties: List[Union[EventPropertyFilter, PersonPropertyFilter]] response: Optional[WebOverviewQueryResponse] = None @@ -970,7 +970,7 @@ class WebStatsTableQuery(BaseModel): breakdownBy: WebStatsBreakdown dateRange: Optional[DateRange] = None kind: Literal["WebStatsTableQuery"] = "WebStatsTableQuery" - properties: List[Union[EventPropertyFilter, HogQLPropertyFilter]] + properties: List[Union[EventPropertyFilter, PersonPropertyFilter]] response: Optional[WebStatsTableQueryResponse] = None @@ -980,7 +980,7 @@ class WebTopClicksQuery(BaseModel): ) dateRange: Optional[DateRange] = None kind: Literal["WebTopClicksQuery"] = "WebTopClicksQuery" - properties: List[Union[EventPropertyFilter, HogQLPropertyFilter]] + properties: List[Union[EventPropertyFilter, PersonPropertyFilter]] response: Optional[WebTopClicksQueryResponse] = None