diff --git a/frontend/src/queries/nodes/InsightViz/InsightContainer.tsx b/frontend/src/queries/nodes/InsightViz/InsightContainer.tsx index f18ae2daf0c07..b7148df4b6b66 100644 --- a/frontend/src/queries/nodes/InsightViz/InsightContainer.tsx +++ b/frontend/src/queries/nodes/InsightViz/InsightContainer.tsx @@ -36,15 +36,6 @@ import { FunnelCorrelation } from 'scenes/insights/views/Funnels/FunnelCorrelati import { InsightResultMetadata } from './InsightResultMetadata' import { Link } from '@posthog/lemon-ui' -const VIEW_MAP = { - [`${InsightType.TRENDS}`]: , - [`${InsightType.STICKINESS}`]: , - [`${InsightType.LIFECYCLE}`]: , - [`${InsightType.FUNNELS}`]: , - [`${InsightType.RETENTION}`]: , - [`${InsightType.PATHS}`]: , -} - export function InsightContainer({ disableHeader, disableTable, @@ -82,11 +73,11 @@ export function InsightContainer({ trendsFilter, funnelsFilter, supportsDisplay, - isUsingSessionAnalysis, samplingFactor, insightDataLoading, erroredQueryId, timedOutQueryId, + shouldShowSessionAnalysisWarning, } = useValues(insightVizDataLogic(insightProps)) const { exportContext } = useValues(insightDataLogic(insightProps)) @@ -135,6 +126,25 @@ export function InsightContainer({ return null })() + function renderActiveView(): JSX.Element | null { + switch (activeView) { + case InsightType.TRENDS: + return + case InsightType.STICKINESS: + return + case InsightType.LIFECYCLE: + return + case InsightType.FUNNELS: + return + case InsightType.RETENTION: + return + case InsightType.PATHS: + return + default: + return null + } + } + function renderTable(): JSX.Element | null { if ( isFunnels && @@ -197,7 +207,7 @@ export function InsightContainer({ return ( <> - {isUsingSessionAnalysis ? ( + {shouldShowSessionAnalysisWarning ? (
When using sessions and session properties, events without session IDs will be excluded from the @@ -255,13 +265,13 @@ export function InsightContainer({ BlockingEmptyState ) : supportsDisplay && showLegend ? (
-
{VIEW_MAP[activeView]}
+
{renderActiveView()}
) : ( - VIEW_MAP[activeView] + renderActiveView() )}
)} diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 190898ed03a60..c626b70d76d06 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -1686,6 +1686,9 @@ "description": "Show with most visual options enabled. Used in insight scene.", "type": "boolean" }, + "hidePersonsModal": { + "type": "boolean" + }, "kind": { "const": "InsightVizNode", "type": "string" @@ -1713,6 +1716,9 @@ }, "source": { "$ref": "#/definitions/InsightQueryNode" + }, + "suppressSessionAnalysisWarning": { + "type": "boolean" } }, "required": ["kind", "source"], @@ -2528,6 +2534,9 @@ "description": "Show with most visual options enabled. Used in insight scene.", "type": "boolean" }, + "hidePersonsModal": { + "type": "boolean" + }, "kind": { "const": "SavedInsightNode", "type": "string" @@ -2619,6 +2628,9 @@ "showTimings": { "description": "Show a detailed query timing breakdown", "type": "boolean" + }, + "suppressSessionAnalysisWarning": { + "type": "boolean" } }, "required": ["kind", "shortId"], diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index fcd957f9adf55..6e8fab0a961c9 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -397,6 +397,8 @@ interface InsightVizNodeViewProps { showResults?: boolean /** Query is embedded inside another bordered component */ embedded?: boolean + suppressSessionAnalysisWarning?: boolean + hidePersonsModal?: boolean } /** Base class for insight query nodes. Should not be used directly. */ diff --git a/frontend/src/queries/types.ts b/frontend/src/queries/types.ts index 1a77b7536d1e0..f1e63d8f54549 100644 --- a/frontend/src/queries/types.ts +++ b/frontend/src/queries/types.ts @@ -1,4 +1,4 @@ -import { InsightLogicProps } from '~/types' +import { ChartDisplayType, InsightLogicProps, TrendResult } from '~/types' import { ComponentType, HTMLProps } from 'react' import { DataTableNode } from '~/queries/schema' @@ -15,6 +15,15 @@ export interface QueryContext { emptyStateHeading?: string emptyStateDetail?: string rowProps?: (record: unknown) => Omit, 'key'> + /** chart-specific rendering context **/ + chartRenderingMetadata?: ChartRenderingMetadata +} + +/** Pass custom rendering metadata to specific kinds of charts **/ +export interface ChartRenderingMetadata { + [ChartDisplayType.WorldMap]?: { + countryProps?: (countryCode: string, countryData: TrendResult | undefined) => Omit, 'key'> + } } export type QueryContextColumnTitleComponent = ComponentType<{ diff --git a/frontend/src/scenes/insights/insightLogic.ts b/frontend/src/scenes/insights/insightLogic.ts index a38becc02327b..825d1a1237265 100644 --- a/frontend/src/scenes/insights/insightLogic.ts +++ b/frontend/src/scenes/insights/insightLogic.ts @@ -51,6 +51,7 @@ import { isInsightVizNode } from '~/queries/utils' import { userLogic } from 'scenes/userLogic' import { transformLegacyHiddenLegendKeys } from 'scenes/funnels/funnelUtils' import { summarizeInsight } from 'scenes/insights/summarizeInsight' +import { InsightVizNode } from '~/queries/schema' const IS_TEST_MODE = process.env.NODE_ENV === 'test' export const UNSAVED_INSIGHT_MIN_REFRESH_INTERVAL_MINUTES = 3 @@ -540,6 +541,7 @@ export const insightLogic = kea([ ) }, ], + showPersonsModal: [() => [(_, p) => p.query], (query?: InsightVizNode) => !query || !query.hidePersonsModal], }), listeners(({ actions, selectors, values }) => ({ setFiltersMerge: ({ filters }) => { diff --git a/frontend/src/scenes/insights/insightVizDataLogic.ts b/frontend/src/scenes/insights/insightVizDataLogic.ts index 2297552a700dc..bbda9f7e73151 100644 --- a/frontend/src/scenes/insights/insightVizDataLogic.ts +++ b/frontend/src/scenes/insights/insightVizDataLogic.ts @@ -171,7 +171,11 @@ export const insightVizDataLogic = kea([ ) }, ], - + shouldShowSessionAnalysisWarning: [ + (s) => [s.isUsingSessionAnalysis, s.query], + (isUsingSessionAnalysis, query) => + isUsingSessionAnalysis && !(isInsightVizNode(query) && query.suppressSessionAnalysisWarning), + ], isNonTimeSeriesDisplay: [ (s) => [s.display], (display) => !!display && NON_TIME_SERIES_DISPLAY_TYPES.includes(display), diff --git a/frontend/src/scenes/insights/views/WorldMap/WorldMap.tsx b/frontend/src/scenes/insights/views/WorldMap/WorldMap.tsx index 7857ddbf83374..472ad00a1aaeb 100644 --- a/frontend/src/scenes/insights/views/WorldMap/WorldMap.tsx +++ b/frontend/src/scenes/insights/views/WorldMap/WorldMap.tsx @@ -1,7 +1,7 @@ import { useValues, useActions } from 'kea' -import React, { useEffect, useRef } from 'react' +import React, { HTMLProps, useEffect, useRef } from 'react' import { insightLogic } from 'scenes/insights/insightLogic' -import { ChartParams, TrendResult } from '~/types' +import { ChartDisplayType, ChartParams, TrendResult } from '~/types' import './WorldMap.scss' import { InsightTooltip } from 'scenes/insights/InsightTooltip/InsightTooltip' import { SeriesDatum } from '../../InsightTooltip/insightTooltipUtils' @@ -104,6 +104,10 @@ interface WorldMapSVGProps extends ChartParams { showTooltip: (countryCode: string, countrySeries: TrendResult | null) => void hideTooltip: () => void updateTooltipCoordinates: (x: number, y: number) => void + worldMapCountryProps?: ( + countryCode: string, + countrySeries: TrendResult | undefined + ) => Omit, 'key'> } const WorldMapSVG = React.memo( @@ -116,6 +120,7 @@ const WorldMapSVG = React.memo( showTooltip, hideTooltip, updateTooltipCoordinates, + worldMapCountryProps, }, ref ) => { @@ -139,15 +144,23 @@ const WorldMapSVG = React.memo( const fill = aggregatedValue ? gradateColor(BRAND_BLUE_HSL, aggregatedValue / maxAggregatedValue, SATURATION_FLOOR) : undefined - return React.cloneElement(countryElement, { - key: countryCode, - style: { color: fill, cursor: showPersonsModal && countrySeries ? 'pointer' : undefined }, - onMouseEnter: () => showTooltip(countryCode, countrySeries || null), - onMouseLeave: () => hideTooltip(), - onMouseMove: (e: MouseEvent) => { - updateTooltipCoordinates(e.clientX, e.clientY) - }, - onClick: () => { + + const { + onClick: propsOnClick, + style, + ...props + } = worldMapCountryProps + ? worldMapCountryProps(countryCode, countrySeries) + : { onClick: undefined, style: undefined } + + let onClick: typeof propsOnClick + if (propsOnClick) { + onClick = (e) => { + propsOnClick(e) + hideTooltip() + } + } else if (showPersonsModal && countrySeries) { + onClick = () => { if (showPersonsModal && countrySeries) { if (countrySeries.persons?.url) { openPersonsModal({ @@ -167,7 +180,19 @@ const WorldMapSVG = React.memo( }) } } + } + } + + return React.cloneElement(countryElement, { + key: countryCode, + style: { color: fill, cursor: onClick ? 'pointer' : undefined, ...style }, + onMouseEnter: () => showTooltip(countryCode, countrySeries || null), + onMouseLeave: () => hideTooltip(), + onMouseMove: (e: MouseEvent) => { + updateTooltipCoordinates(e.clientX, e.clientY) }, + onClick, + ...props, }) })} @@ -176,10 +201,11 @@ const WorldMapSVG = React.memo( ) ) -export function WorldMap({ showPersonsModal = true }: ChartParams): JSX.Element { +export function WorldMap({ showPersonsModal = true, context }: ChartParams): JSX.Element { const { insightProps } = useValues(insightLogic) const { countryCodeToSeries, maxAggregatedValue } = useValues(worldMapLogic(insightProps)) const { showTooltip, hideTooltip, updateTooltipCoordinates } = useActions(worldMapLogic(insightProps)) + const renderingMetadata = context?.chartRenderingMetadata?.[ChartDisplayType.WorldMap] const svgRef = useWorldMapTooltip(showPersonsModal) @@ -192,6 +218,7 @@ export function WorldMap({ showPersonsModal = true }: ChartParams): JSX.Element hideTooltip={hideTooltip} updateTooltipCoordinates={updateTooltipCoordinates} ref={svgRef} + worldMapCountryProps={renderingMetadata?.countryProps} /> ) } diff --git a/frontend/src/scenes/trends/Trends.tsx b/frontend/src/scenes/trends/Trends.tsx index e73da5d617fda..8938d477173f1 100644 --- a/frontend/src/scenes/trends/Trends.tsx +++ b/frontend/src/scenes/trends/Trends.tsx @@ -8,14 +8,16 @@ import { WorldMap } from 'scenes/insights/views/WorldMap' import { BoldNumber } from 'scenes/insights/views/BoldNumber' import { LemonButton } from '@posthog/lemon-ui' import { trendsDataLogic } from './trendsDataLogic' +import { QueryContext } from '~/queries/types' interface Props { view: InsightType + context?: QueryContext } -export function TrendInsight({ view }: Props): JSX.Element { +export function TrendInsight({ view, context }: Props): JSX.Element { const { insightMode } = useValues(insightSceneLogic) - const { insightProps } = useValues(insightLogic) + const { insightProps, showPersonsModal } = useValues(insightLogic) const { display, series, breakdown, loadMoreBreakdownUrl, breakdownValuesLoading } = useValues( trendsDataLogic(insightProps) @@ -30,10 +32,10 @@ export function TrendInsight({ view }: Props): JSX.Element { display === ChartDisplayType.ActionsAreaGraph || display === ChartDisplayType.ActionsBar ) { - return + return } if (display === ChartDisplayType.BoldNumber) { - return + return } if (display === ChartDisplayType.ActionsTable) { const ActionsTable = InsightsTable @@ -47,13 +49,13 @@ export function TrendInsight({ view }: Props): JSX.Element { ) } if (display === ChartDisplayType.ActionsPie) { - return + return } if (display === ChartDisplayType.ActionsBarValue) { - return + return } if (display === ChartDisplayType.WorldMap) { - return + return } } diff --git a/frontend/src/scenes/web-analytics/WebAnalyticsDataTable.tsx b/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx similarity index 87% rename from frontend/src/scenes/web-analytics/WebAnalyticsDataTable.tsx rename to frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx index 2354af9ce805d..815e57d583d58 100644 --- a/frontend/src/scenes/web-analytics/WebAnalyticsDataTable.tsx +++ b/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx @@ -1,5 +1,5 @@ import { QueryContext, QueryContextColumnComponent, QueryContextColumnTitleComponent } from '~/queries/types' -import { DataTableNode, NodeKind, WebStatsBreakdown } from '~/queries/schema' +import { DataTableNode, InsightVizNode, NodeKind, WebStatsBreakdown } from '~/queries/schema' import { UnexpectedNeverError } from 'lib/utils' import { useActions } from 'kea' import { webAnalyticsLogic } from 'scenes/web-analytics/webAnalyticsLogic' @@ -7,6 +7,7 @@ import { useCallback, useMemo } from 'react' import { Query } from '~/queries/Query/Query' import { countryCodeToFlag, countryCodeToName } from 'scenes/insights/views/WorldMap' import { PropertyFilterType } from '~/types' +import { ChartDisplayType } from '~/types' const PercentageCell: QueryContextColumnComponent = ({ value }) => { if (typeof value === 'number') { @@ -171,6 +172,32 @@ export const webAnalyticsDataTableQueryContext: QueryContext = { }, } +export const WebStatsTrendTile = ({ query }: { query: InsightVizNode }): JSX.Element => { + const { togglePropertyFilter } = useActions(webAnalyticsLogic) + const { key: worldMapPropertyName } = webStatsBreakdownToPropertyName(WebStatsBreakdown.Country) + const onWorldMapClick = useCallback( + (breakdownValue: string) => { + togglePropertyFilter(PropertyFilterType.Event, worldMapPropertyName, breakdownValue) + }, + [togglePropertyFilter, worldMapPropertyName] + ) + + const context = useMemo((): QueryContext => { + return { + ...webAnalyticsDataTableQueryContext, + chartRenderingMetadata: { + [ChartDisplayType.WorldMap]: { + countryProps: (countryCode, values) => ({ + onClick: values && values.count > 0 ? () => onWorldMapClick(countryCode) : undefined, + }), + }, + }, + } + }, [onWorldMapClick]) + + return +} + export const WebStatsTableTile = ({ query, breakdownBy, diff --git a/frontend/src/scenes/web-analytics/WebDashboard.tsx b/frontend/src/scenes/web-analytics/WebDashboard.tsx index 2f32fd815ec48..48efb7c9529a9 100644 --- a/frontend/src/scenes/web-analytics/WebDashboard.tsx +++ b/frontend/src/scenes/web-analytics/WebDashboard.tsx @@ -6,7 +6,11 @@ import { isEventPropertyOrPersonPropertyFilter } from 'lib/components/PropertyFi import { NodeKind, QuerySchema } from '~/queries/schema' import { DateFilter } from 'lib/components/DateFilter/DateFilter' import { WebAnalyticsNotice } from 'scenes/web-analytics/WebAnalyticsNotice' -import { webAnalyticsDataTableQueryContext, WebStatsTableTile } from 'scenes/web-analytics/WebAnalyticsDataTable' +import { + webAnalyticsDataTableQueryContext, + WebStatsTableTile, + WebStatsTrendTile, +} from 'scenes/web-analytics/WebAnalyticsTile' import { WebTabs } from 'scenes/web-analytics/WebTabs' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' @@ -115,6 +119,9 @@ const WebQuery = ({ query }: { query: QuerySchema }): JSX.Element => { if (query.kind === NodeKind.DataTableNode && query.source.kind === NodeKind.WebStatsTableQuery) { return } + if (query.kind === NodeKind.InsightVizNode) { + return + } return } diff --git a/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts b/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts index fcf6e11dbcdf9..5882c44e45d88 100644 --- a/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts +++ b/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts @@ -38,6 +38,12 @@ export interface TabsTile extends BaseTile { export type WebDashboardTile = QueryTile | TabsTile +export enum GraphsTab { + UNIQUE_USERS = 'UNIQUE_USERS', + PAGE_VIEWS = 'PAGE_VIEWS', + NUM_SESSION = 'NUM_SESSION', +} + export enum SourceTab { REFERRING_DOMAIN = 'REFERRING_DOMAIN', UTM_SOURCE = 'UTM_SOURCE', @@ -78,6 +84,9 @@ export const webAnalyticsLogic = kea([ key, value, }), + setGraphsTab: (tab: string) => ({ + tab, + }), setSourceTab: (tab: string) => ({ tab, }), @@ -140,6 +149,12 @@ export const webAnalyticsLogic = kea([ }, }, ], + graphsTab: [ + GraphsTab.UNIQUE_USERS as string, + { + setGraphsTab: (_, { tab }) => tab, + }, + ], sourceTab: [ SourceTab.REFERRING_DOMAIN as string, { @@ -159,7 +174,7 @@ export const webAnalyticsLogic = kea([ }, ], geographyTab: [ - GeographyTab.COUNTRIES as string, + GeographyTab.MAP as string, { setGeographyTab: (_, { tab }) => tab, }, @@ -179,9 +194,19 @@ export const webAnalyticsLogic = kea([ }), selectors(({ actions }) => ({ tiles: [ - (s) => [s.webAnalyticsFilters, s.pathTab, s.deviceTab, s.sourceTab, s.geographyTab, s.dateFrom, s.dateTo], + (s) => [ + s.webAnalyticsFilters, + s.graphsTab, + s.pathTab, + s.deviceTab, + s.sourceTab, + s.geographyTab, + s.dateFrom, + s.dateTo, + ], ( webAnalyticsFilters, + graphsTab, pathTab, deviceTab, sourceTab, @@ -204,6 +229,100 @@ export const webAnalyticsLogic = kea([ dateRange, }, }, + { + layout: { + colSpan: 6, + }, + activeTabId: graphsTab, + setTabId: actions.setGraphsTab, + tabs: [ + { + id: GraphsTab.UNIQUE_USERS, + title: 'Unique Visitors', + linkText: 'Visitors', + query: { + kind: NodeKind.InsightVizNode, + source: { + kind: NodeKind.TrendsQuery, + dateRange, + interval: 'day', + series: [ + { + event: '$pageview', + kind: NodeKind.EventsNode, + math: BaseMathType.UniqueUsers, + name: '$pageview', + }, + ], + trendsFilter: { + compare: true, + display: ChartDisplayType.ActionsLineGraph, + }, + filterTestAccounts: true, + properties: webAnalyticsFilters, + }, + hidePersonsModal: true, + }, + }, + { + id: GraphsTab.PAGE_VIEWS, + title: 'Page Views', + linkText: 'Views', + query: { + kind: NodeKind.InsightVizNode, + source: { + kind: NodeKind.TrendsQuery, + dateRange, + interval: 'day', + series: [ + { + event: '$pageview', + kind: NodeKind.EventsNode, + math: BaseMathType.TotalCount, + name: '$pageview', + }, + ], + trendsFilter: { + compare: true, + display: ChartDisplayType.ActionsLineGraph, + }, + filterTestAccounts: true, + properties: webAnalyticsFilters, + }, + hidePersonsModal: true, + }, + }, + { + id: GraphsTab.NUM_SESSION, + title: 'Sessions', + linkText: 'Sessions', + query: { + kind: NodeKind.InsightVizNode, + source: { + kind: NodeKind.TrendsQuery, + dateRange, + interval: 'day', + series: [ + { + event: '$pageview', + kind: NodeKind.EventsNode, + math: BaseMathType.UniqueSessions, + name: '$pageview', + }, + ], + trendsFilter: { + compare: true, + display: ChartDisplayType.ActionsLineGraph, + }, + filterTestAccounts: true, + properties: webAnalyticsFilters, + }, + suppressSessionAnalysisWarning: true, + hidePersonsModal: true, + }, + }, + ], + }, { layout: { colSpan: 6, @@ -351,34 +470,6 @@ export const webAnalyticsLogic = kea([ }, ], }, - // { - // title: 'Unique visitors', - // layout: { - // colSpan: 6, - // }, - // query: { - // kind: NodeKind.InsightVizNode, - // source: { - // kind: NodeKind.TrendsQuery, - // dateRange, - // interval: 'day', - // series: [ - // { - // event: '$pageview', - // kind: NodeKind.EventsNode, - // math: BaseMathType.UniqueUsers, - // name: '$pageview', - // }, - // ], - // trendsFilter: { - // compare: true, - // display: ChartDisplayType.ActionsLineGraph, - // }, - // filterTestAccounts: true, - // properties: webAnalyticsFilters, - // }, - // }, - // }, { layout: { colSpan: 6, @@ -412,6 +503,7 @@ export const webAnalyticsLogic = kea([ filterTestAccounts: true, properties: webAnalyticsFilters, }, + hidePersonsModal: true, }, }, { diff --git a/posthog/schema.py b/posthog/schema.py index a73c5f22c0e45..77bbd8cb01cb3 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -481,6 +481,7 @@ class SavedInsightNode(BaseModel): full: Optional[bool] = Field( default=None, description="Show with most visual options enabled. Used in insight scene." ) + hidePersonsModal: Optional[bool] = None kind: Literal["SavedInsightNode"] = "SavedInsightNode" propertiesViaUrl: Optional[bool] = Field(default=None, description="Link properties via the URL (default: false)") shortId: str @@ -514,6 +515,7 @@ class SavedInsightNode(BaseModel): showSearch: Optional[bool] = Field(default=None, description="Include a free text search field (PersonsNode only)") showTable: Optional[bool] = None showTimings: Optional[bool] = Field(default=None, description="Show a detailed query timing breakdown") + suppressSessionAnalysisWarning: Optional[bool] = None class SessionPropertyFilter(BaseModel): @@ -1712,6 +1714,7 @@ class InsightVizNode(BaseModel): full: Optional[bool] = Field( default=None, description="Show with most visual options enabled. Used in insight scene." ) + hidePersonsModal: Optional[bool] = None kind: Literal["InsightVizNode"] = "InsightVizNode" showCorrelationTable: Optional[bool] = None showFilters: Optional[bool] = None @@ -1721,6 +1724,7 @@ class InsightVizNode(BaseModel): showResults: Optional[bool] = None showTable: Optional[bool] = None source: Union[TrendsQuery, FunnelsQuery, RetentionQuery, PathsQuery, StickinessQuery, LifecycleQuery] + suppressSessionAnalysisWarning: Optional[bool] = None class InsightPersonsQuery(BaseModel):