Skip to content

Commit

Permalink
feat(web-analytics): Use person properties for web analytics source (#…
Browse files Browse the repository at this point in the history
…18423)

* Use person properties for web analytics source

* Help kea typegen out

* Fix query runner types

* Remove pointless comment
  • Loading branch information
robbie-c authored Nov 8, 2023
1 parent 9fb1545 commit be184b6
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 161 deletions.
5 changes: 5 additions & 0 deletions frontend/src/lib/components/PropertyFilters/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,13 @@ export const taxonomicFilterLogic = kea<taxonomicFilterLogicType>([
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),
},
{
Expand Down
19 changes: 11 additions & 8 deletions frontend/src/queries/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/queries/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import {
FunnelsFilterType,
GroupMathType,
HogQLMathType,
HogQLPropertyFilter,
InsightShortId,
IntervalType,
LifecycleFilterType,
LifecycleToggle,
PathsFilterType,
PersonPropertyFilter,
PropertyGroupFilter,
PropertyMathType,
RetentionFilterType,
Expand Down Expand Up @@ -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
Expand Down
39 changes: 21 additions & 18 deletions frontend/src/scenes/web-analytics/WebAnalyticsDataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 => {
Expand Down
35 changes: 25 additions & 10 deletions frontend/src/scenes/web-analytics/WebDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -18,8 +18,13 @@ const Filters = (): JSX.Element => {
<div className="flex flex-row flex-wrap gap-2">
<DateFilter dateFrom={dateFrom} dateTo={dateTo} onChange={setDates} />
<PropertyFilters
taxonomicGroupTypes={[TaxonomicFilterGroupType.EventProperties]}
onChange={(filters) => setWebAnalyticsFilters(filters.filter(isEventPropertyFilter))}
taxonomicGroupTypes={[
TaxonomicFilterGroupType.EventProperties,
TaxonomicFilterGroupType.PersonProperties,
]}
onChange={(filters) =>
setWebAnalyticsFilters(filters.filter(isEventPropertyOrPersonPropertyFilter))
}
propertyFilters={webAnalyticsFilters}
pageKey={'web-analytics'}
eventNames={['$pageview', '$pageleave', '$autocapture']}
Expand All @@ -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',
],
}}
/>
Expand Down
124 changes: 45 additions & 79 deletions frontend/src/scenes/web-analytics/webAnalyticsLogic.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<webAnalyticsLogicType>([
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,
}),
Expand All @@ -93,80 +95,44 @@ export const webAnalyticsLogic = kea<webAnalyticsLogicType>([
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]
Expand Down
Loading

0 comments on commit be184b6

Please sign in to comment.