From b96f7760dd824dedcfedbe9e4057751498e8576b Mon Sep 17 00:00:00 2001 From: Robbie Coomber Date: Fri, 15 Dec 2023 13:55:16 +0000 Subject: [PATCH] Add channel to web dashboard --- frontend/src/queries/schema.json | 1 + frontend/src/queries/schema.ts | 1 + .../scenes/web-analytics/WebAnalyticsTile.tsx | 23 +++++-- frontend/src/scenes/web-analytics/WebTabs.tsx | 27 +++++--- .../scenes/web-analytics/webAnalyticsLogic.ts | 68 ++++++++++++++++++- posthog/hogql/printer.py | 2 +- .../web_analytics/stats_table.py | 63 +++++++++++++++++ posthog/schema.py | 1 + 8 files changed, 170 insertions(+), 16 deletions(-) diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 91c43c38ac0ee5..d82e911edd86e7 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -3443,6 +3443,7 @@ "enum": [ "Page", "InitialPage", + "InitialChannelType", "InitialReferringDomain", "InitialUTMSource", "InitialUTMCampaign", diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 267b227a0625c4..ec07dad2e1c279 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -711,6 +711,7 @@ export enum WebStatsBreakdown { Page = 'Page', InitialPage = 'InitialPage', // ExitPage = 'ExitPage' + InitialChannelType = 'InitialChannelType', InitialReferringDomain = 'InitialReferringDomain', InitialUTMSource = 'InitialUTMSource', InitialUTMCampaign = 'InitialUTMCampaign', diff --git a/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx b/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx index ba25a3ec372ae3..74a7e8b156efdd 100644 --- a/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx +++ b/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx @@ -34,6 +34,8 @@ const BreakdownValueTitle: QueryContextColumnTitleComponent = (props) => { return <>Path case WebStatsBreakdown.InitialPage: return <>Initial Path + case WebStatsBreakdown.InitialChannelType: + return <>Initial Channel Type case WebStatsBreakdown.InitialReferringDomain: return <>Referring Domain case WebStatsBreakdown.InitialUTMSource: @@ -114,12 +116,14 @@ const BreakdownValueCell: QueryContextColumnComponent = (props) => { export const webStatsBreakdownToPropertyName = ( breakdownBy: WebStatsBreakdown -): { key: string; type: PropertyFilterType.Person | PropertyFilterType.Event } => { +): { key: string; type: PropertyFilterType.Person | PropertyFilterType.Event } | undefined => { switch (breakdownBy) { case WebStatsBreakdown.Page: return { key: '$pathname', type: PropertyFilterType.Event } case WebStatsBreakdown.InitialPage: return { key: '$initial_pathname', type: PropertyFilterType.Person } + case WebStatsBreakdown.InitialChannelType: + return undefined case WebStatsBreakdown.InitialReferringDomain: return { key: '$initial_referring_domain', type: PropertyFilterType.Person } case WebStatsBreakdown.InitialUTMSource: @@ -189,11 +193,14 @@ export const WebStatsTrendTile = ({ hasOSFilter, dateFilter: { interval }, } = useValues(webAnalyticsLogic) - const { key: worldMapPropertyName } = webStatsBreakdownToPropertyName(WebStatsBreakdown.Country) - const { key: deviceTypePropertyName } = webStatsBreakdownToPropertyName(WebStatsBreakdown.DeviceType) + const worldMapPropertyName = webStatsBreakdownToPropertyName(WebStatsBreakdown.Country)?.key + const deviceTypePropertyName = webStatsBreakdownToPropertyName(WebStatsBreakdown.DeviceType)?.key const onWorldMapClick = useCallback( (breakdownValue: string) => { + if (!worldMapPropertyName) { + return + } togglePropertyFilter(PropertyFilterType.Event, worldMapPropertyName, breakdownValue) if (!hasCountryFilter) { // if we just added a country filter, switch to the region tab, as the world map will not be useful @@ -216,6 +223,9 @@ export const WebStatsTrendTile = ({ if (!breakdownValue) { return } + if (!deviceTypePropertyName) { + return + } togglePropertyFilter(PropertyFilterType.Event, deviceTypePropertyName, breakdownValue) // switch to a different tab if we can, try them in this order: DeviceType Browser OS @@ -283,10 +293,13 @@ export const WebStatsTableTile = ({ breakdownBy: WebStatsBreakdown }): JSX.Element => { const { togglePropertyFilter } = useActions(webAnalyticsLogic) - const { key, type } = webStatsBreakdownToPropertyName(breakdownBy) + const { key, type } = webStatsBreakdownToPropertyName(breakdownBy) || {} const onClick = useCallback( (breakdownValue: string) => { + if (!key || !type) { + return + } togglePropertyFilter(type, key, breakdownValue) }, [togglePropertyFilter, type, key] @@ -299,7 +312,7 @@ export const WebStatsTableTile = ({ return {} } return { - onClick: () => onClick(breakdownValue), + onClick: key && type ? () => onClick(breakdownValue) : undefined, } } return { diff --git a/frontend/src/scenes/web-analytics/WebTabs.tsx b/frontend/src/scenes/web-analytics/WebTabs.tsx index 6059b827d35dc7..c6867520b07d06 100644 --- a/frontend/src/scenes/web-analytics/WebTabs.tsx +++ b/frontend/src/scenes/web-analytics/WebTabs.tsx @@ -1,4 +1,4 @@ -import { LemonTabs } from '@posthog/lemon-ui' +import { LemonSelect, LemonTabs } from '@posthog/lemon-ui' import clsx from 'clsx' import React from 'react' @@ -19,13 +19,24 @@ export const WebTabs = ({
{

{activeTab?.title}

} - ({ key: id, label: linkText }))} - /> + {tabs.length > 3 ? ( + ({ value: id, label: linkText }))} + /> + ) : ( + ({ key: id, label: linkText }))} + /> + )}
{activeTab?.content}
diff --git a/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts b/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts index 789be7d2ba9397..66048d2fb49f53 100644 --- a/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts +++ b/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts @@ -65,8 +65,12 @@ export enum GraphsTab { export enum SourceTab { REFERRING_DOMAIN = 'REFERRING_DOMAIN', + CHANNEL = 'CHANNEL', UTM_SOURCE = 'UTM_SOURCE', + UTM_MEDIUM = 'UTM_MEDIUM', UTM_CAMPAIGN = 'UTM_CAMPAIGN', + UTM_CONTENT = 'UTM_CONTENT', + UTM_TERM = 'UTM_TERM', } export enum DeviceTab { @@ -427,7 +431,7 @@ export const webAnalyticsLogic = kea([ { id: SourceTab.REFERRING_DOMAIN, title: 'Top referrers', - linkText: 'Referrer', + linkText: 'Referrering domain', query: { full: true, kind: NodeKind.DataTableNode, @@ -439,6 +443,21 @@ export const webAnalyticsLogic = kea([ }, }, }, + { + id: SourceTab.CHANNEL, + title: 'Top channels', + linkText: 'Channel', + query: { + full: true, + kind: NodeKind.DataTableNode, + source: { + kind: NodeKind.WebStatsTableQuery, + properties: webAnalyticsFilters, + breakdownBy: WebStatsBreakdown.InitialChannelType, + dateRange, + }, + }, + }, { id: SourceTab.UTM_SOURCE, title: 'Top sources', @@ -454,9 +473,24 @@ export const webAnalyticsLogic = kea([ }, }, }, + { + id: SourceTab.UTM_MEDIUM, + title: 'Top UTM medium', + linkText: 'UTM medium', + query: { + full: true, + kind: NodeKind.DataTableNode, + source: { + kind: NodeKind.WebStatsTableQuery, + properties: webAnalyticsFilters, + breakdownBy: WebStatsBreakdown.InitialUTMMedium, + dateRange, + }, + }, + }, { id: SourceTab.UTM_CAMPAIGN, - title: 'Top campaigns', + title: 'Top UTM campaigns', linkText: 'UTM campaign', query: { full: true, @@ -469,6 +503,36 @@ export const webAnalyticsLogic = kea([ }, }, }, + { + id: SourceTab.UTM_CONTENT, + title: 'Top UTM content', + linkText: 'UTM content', + query: { + full: true, + kind: NodeKind.DataTableNode, + source: { + kind: NodeKind.WebStatsTableQuery, + properties: webAnalyticsFilters, + breakdownBy: WebStatsBreakdown.InitialUTMContent, + dateRange, + }, + }, + }, + { + id: SourceTab.UTM_TERM, + title: 'Top UTM terms', + linkText: 'UTM term', + query: { + full: true, + kind: NodeKind.DataTableNode, + source: { + kind: NodeKind.WebStatsTableQuery, + properties: webAnalyticsFilters, + breakdownBy: WebStatsBreakdown.InitialUTMTerm, + dateRange, + }, + }, + }, ], }, { diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 8d7883c77c059f..21f9a68bab22ea 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -842,7 +842,7 @@ def visit_call(self, node: ast.Call): validate_function_args(node.args, func_meta.min_args, func_meta.max_args, node.name) args = [self.visit(arg) for arg in node.args] - if self.dialect == "clickhouse": + if self.dialect in ("hogql", "clickhouse"): if node.name == "hogql_lookupDomainType": return f"dictGetOrNull('channel_definition_dict', 'domain_type', (cutToFirstSignificantSubdomain(coalesce({args[0]}, '')), 'source'))" elif node.name == "hogql_lookupPaidDomainType": diff --git a/posthog/hogql_queries/web_analytics/stats_table.py b/posthog/hogql_queries/web_analytics/stats_table.py index bed6ebeee7170f..3fc76041e252f4 100644 --- a/posthog/hogql_queries/web_analytics/stats_table.py +++ b/posthog/hogql_queries/web_analytics/stats_table.py @@ -93,6 +93,67 @@ def counts_breakdown(self): match self.query.breakdownBy: case WebStatsBreakdown.Page: return ast.Field(chain=["properties", "$pathname"]) + case WebStatsBreakdown.InitialChannelType: + # use this for now, switch to person.$virt_initial_channel_type when it's working + return parse_expr( + """ +multiIf( + match(person.properties.$initial_utm_campaign, 'cross-network'), + 'Cross Network', + + ( + match(person.properties.$initial_utm_medium, '^(.*cp.*|ppc|retargeting|paid.*)$') OR + properties.$initial_gclid IS NOT NULL OR + properties.$initial_gad_source IS NOT NULL + ), + coalesce( + hogql_lookupPaidSourceType(person.properties.$initial_utm_source), + hogql_lookupPaidDomainType(person.properties.$initial_referring_domain), + if( + match(properties.$initial_utm_campaign, '^(.*(([^a-df-z]|^)shop|shopping).*)$'), + 'Paid Shopping', + NULL + ), + hogql_lookupPaidMediumType(person.properties.$initial_utm_medium), + multiIf ( + person.properties.$initial_gad_source = '1', + 'Paid Search', + + match(person.properties.$initial_utm_campaign, '^(.*video.*)$'), + 'Paid Video', + + 'Paid Other' + ) + ), + + ( + person.properties.$initial_referring_domain = '$direct' + AND (person.properties.$initial_utm_medium IS NULL OR person.properties.$initial_utm_medium = '') + AND (person.properties.$initial_utm_source IS NULL OR person.properties.$initial_utm_source IN ('', '(direct)', 'direct')) + ), + 'Direct', + + coalesce( + hogql_lookupOrganicSourceType(person.properties.$initial_utm_source), + hogql_lookupOrganicDomainType(person.properties.$initial_referring_domain), + if( + match(person.properties.$initial_utm_campaign, '^(.*(([^a-df-z]|^)shop|shopping).*)$'), + 'Organic Shopping', + NULL + ), + hogql_lookupOrganicMediumType(person.properties.$initial_utm_medium), + multiIf( + match(person.properties.$initial_utm_campaign, '^(.*video.*)$'), + 'Organic Video', + + match(person.properties.$initial_utm_medium, 'push$'), + 'Push', + + NULL + ) + ) +)""" + ) case WebStatsBreakdown.InitialPage: return ast.Field(chain=["person", "properties", "$initial_pathname"]) case WebStatsBreakdown.InitialReferringDomain: @@ -138,6 +199,8 @@ def where_breakdown(self): return parse_expr('tupleElement("context.columns.breakdown_value", 2) IS NOT NULL') case WebStatsBreakdown.City: return parse_expr('tupleElement("context.columns.breakdown_value", 2) IS NOT NULL') + case WebStatsBreakdown.InitialChannelType: + return parse_expr("TRUE") # actually show null values case WebStatsBreakdown.InitialUTMSource: return parse_expr("TRUE") # actually show null values case WebStatsBreakdown.InitialUTMCampaign: diff --git a/posthog/schema.py b/posthog/schema.py index 89d3e70a56a8d9..d196f954497e9d 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -689,6 +689,7 @@ class WebOverviewQueryResponse(BaseModel): class WebStatsBreakdown(str, Enum): Page = "Page" InitialPage = "InitialPage" + InitialChannelType = "InitialChannelType" InitialReferringDomain = "InitialReferringDomain" InitialUTMSource = "InitialUTMSource" InitialUTMCampaign = "InitialUTMCampaign"