Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web-analytics): Use person properties for web analytics source #18423

Merged
merged 4 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading