diff --git a/frontend/__snapshots__/components-cards-insight-details--trends-world-map--dark.png b/frontend/__snapshots__/components-cards-insight-details--trends-world-map--dark.png index 15b60cc25575c1..6b08cf6fd3d087 100644 Binary files a/frontend/__snapshots__/components-cards-insight-details--trends-world-map--dark.png and b/frontend/__snapshots__/components-cards-insight-details--trends-world-map--dark.png differ diff --git a/frontend/__snapshots__/components-cards-insight-details--trends-world-map--light.png b/frontend/__snapshots__/components-cards-insight-details--trends-world-map--light.png index 06e1bfb343f045..975a66aad5e556 100644 Binary files a/frontend/__snapshots__/components-cards-insight-details--trends-world-map--light.png and b/frontend/__snapshots__/components-cards-insight-details--trends-world-map--light.png differ diff --git a/frontend/src/lib/components/ActivityLog/activityLogLogic.insight.test.tsx b/frontend/src/lib/components/ActivityLog/activityLogLogic.insight.test.tsx index fc98560ef7f207..c86e548e0b8ba7 100644 --- a/frontend/src/lib/components/ActivityLog/activityLogLogic.insight.test.tsx +++ b/frontend/src/lib/components/ActivityLog/activityLogLogic.insight.test.tsx @@ -4,6 +4,7 @@ import { render } from '@testing-library/react' import { MOCK_TEAM_ID } from 'lib/api.mock' import { makeTestSetup } from 'lib/components/ActivityLog/activityLogLogic.test.setup' +import { BreakdownFilter } from '~/queries/schema' import { ActivityScope } from '~/types' jest.mock('lib/colors') @@ -84,81 +85,104 @@ describe('the activity log logic', () => { }) it('can handle change of insight query', async () => { - const logic = await insightTestSetup('test insight', 'updated', [ - { - type: ActivityScope.INSIGHT, - action: 'changed', - field: 'query', - after: { - kind: 'TrendsQuery', - properties: { - type: 'AND', - values: [ - { - type: 'OR', - values: [ - { - type: 'event', - key: '$current_url', - operator: 'exact', - value: ['https://hedgebox.net/files/'], - }, - { - type: 'event', - key: '$geoip_country_code', - operator: 'exact', - value: ['US', 'AU'], - }, - ], - }, - ], - }, - filterTestAccounts: false, - interval: 'day', - dateRange: { - date_from: '-7d', - }, - series: [ + const insightMock = { + type: ActivityScope.INSIGHT, + action: 'changed', + field: 'query', + after: { + kind: 'TrendsQuery', + properties: { + type: 'AND', + values: [ { - kind: 'EventsNode', - name: '$pageview', - custom_name: 'Views', - event: '$pageview', - properties: [ + type: 'OR', + values: [ { type: 'event', - key: '$browser', + key: '$current_url', operator: 'exact', - value: 'Chrome', + value: ['https://hedgebox.net/files/'], }, { - type: 'cohort', - key: 'id', - value: 2, + type: 'event', + key: '$geoip_country_code', + operator: 'exact', + value: ['US', 'AU'], }, ], - limit: 100, }, ], - trendsFilter: { - display: 'ActionsAreaGraph', - }, - breakdownFilter: { - breakdown: '$geoip_country_code', - breakdown_type: 'event', + }, + filterTestAccounts: false, + interval: 'day', + dateRange: { + date_from: '-7d', + }, + series: [ + { + kind: 'EventsNode', + name: '$pageview', + custom_name: 'Views', + event: '$pageview', + properties: [ + { + type: 'event', + key: '$browser', + operator: 'exact', + value: 'Chrome', + }, + { + type: 'cohort', + key: 'id', + value: 2, + }, + ], + limit: 100, }, + ], + trendsFilter: { + display: 'ActionsAreaGraph', + }, + breakdownFilter: { + breakdown: '$geoip_country_code', + breakdown_type: 'event', }, }, - ]) - const actual = logic.values.humanizedActivity + } - const renderedDescription = render(<>{actual[0].description}).container + let logic = await insightTestSetup('test insight', 'updated', [insightMock as any]) + let actual = logic.values.humanizedActivity + + let renderedDescription = render(<>{actual[0].description}).container expect(renderedDescription).toHaveTextContent('peter changed query definition on test insight') - const renderedExtendedDescription = render(<>{actual[0].extendedDescription}).container + let renderedExtendedDescription = render(<>{actual[0].extendedDescription}).container expect(renderedExtendedDescription).toHaveTextContent( "Query summaryAShowing \"Views\"Pageviewcounted by total countwhere event'sBrowser= equals Chromeand person belongs to cohortID 2FiltersEvent'sCurrent URL= equals https://hedgebox.net/files/or event'sCountry Code= equals US or AUBreakdown byCountry Code" ) + ;(insightMock.after.breakdownFilter as BreakdownFilter) = { + breakdowns: [ + { + property: '$geoip_country_code', + type: 'event', + }, + { + property: '$session_duration', + type: 'session', + }, + ], + } + + logic = await insightTestSetup('test insight', 'updated', [insightMock as any]) + actual = logic.values.humanizedActivity + + renderedDescription = render(<>{actual[0].description}).container + expect(renderedDescription).toHaveTextContent('peter changed query definition on test insight') + + renderedExtendedDescription = render(<>{actual[0].extendedDescription}).container + expect(renderedExtendedDescription).toHaveTextContent( + "Query summaryAShowing \"Views\"Pageviewcounted by total countwhere event'sBrowser= equals Chromeand person belongs to cohortID 2FiltersEvent'sCurrent URL= equals https://hedgebox.net/files/or event'sCountry Code= equals US or AUBreakdown byCountry CodeSession duration" + ) }) it('can handle change of filters on a retention graph', async () => { diff --git a/frontend/src/lib/components/ActivityLog/complex.sql b/frontend/src/lib/components/ActivityLog/complex.sql new file mode 100644 index 00000000000000..377778eff333d2 --- /dev/null +++ b/frontend/src/lib/components/ActivityLog/complex.sql @@ -0,0 +1,27 @@ +SELECT + count() AS total, + toStartOfDay(min_timestamp) AS day_start, + breakdown_value AS breakdown_value +FROM + (SELECT + min(timestamp) AS min_timestamp, + argMin(breakdown_value, timestamp) AS breakdown_value + FROM + (SELECT + person_id, + timestamp, + ifNull(nullIf(toString(properties.$browser), ''), '$$_posthog_breakdown_null_$$') AS breakdown_value + FROM + events AS e SAMPLE 1 + WHERE + and(equals(event, '$pageview'), lessOrEquals(timestamp, assumeNotNull(toDateTime('2025-01-20 23:59:59')))) + ) + GROUP BY + person_id + ) +WHERE + greaterOrEquals(min_timestamp, toStartOfDay(assumeNotNull(toDateTime('2020-01-09 00:00:00')))) +GROUP BY + day_start, + breakdown_value +LIMIT 50000 diff --git a/frontend/src/lib/components/ActivityLog/full_query.sql b/frontend/src/lib/components/ActivityLog/full_query.sql new file mode 100644 index 00000000000000..b7129f83c64f46 --- /dev/null +++ b/frontend/src/lib/components/ActivityLog/full_query.sql @@ -0,0 +1,41 @@ +SELECT + arrayMap(number -> plus(toStartOfDay(assumeNotNull(toDateTime('2024-07-16 00:00:00'))), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfDay(assumeNotNull(toDateTime('2024-07-16 00:00:00'))), toStartOfDay(assumeNotNull(toDateTime('2024-07-23 23:59:59'))))), 1))) AS date, + arrayMap(_match_date -> arraySum(arraySlice(groupArray(count), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> equals(x, _match_date), _days_for_count), _index), 1))), date) AS total +FROM + (SELECT + sum(total) AS count, + day_start + FROM (SELECT + count() AS total, + day_start, + breakdown_value + FROM ( + SELECT + min(timestamp) as day_start, + argMin(breakdown_value, timestamp) AS breakdown_value, + FROM + ( + SELECT + person_id, + toStartOfDay(timestamp) AS timestamp, + ifNull(nullIf(toString(person.properties.email), ''), '$$_posthog_breakdown_null_$$') AS breakdown_value + FROM + events AS e SAMPLE 1 + WHERE + and(lessOrEquals(timestamp, assumeNotNull(toDateTime('2024-07-23 23:59:59'))), equals(properties.$browser, 'Safari')) + ) + WHERE + greaterOrEquals(timestamp, toStartOfDay(assumeNotNull(toDateTime('2024-07-16 00:00:00')))) + GROUP BY + person_id + ) + GROUP BY + day_start, + breakdown_value) + GROUP BY + day_start + ORDER BY + day_start ASC) +ORDER BY + arraySum(total) DESC +LIMIT 50000 diff --git a/frontend/src/lib/components/ActivityLog/weekly.sql b/frontend/src/lib/components/ActivityLog/weekly.sql new file mode 100644 index 00000000000000..579249842508c5 --- /dev/null +++ b/frontend/src/lib/components/ActivityLog/weekly.sql @@ -0,0 +1,45 @@ +SELECT + arrayMap(number -> plus(toStartOfDay(assumeNotNull(toDateTime('2024-07-16 00:00:00'))), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfDay(assumeNotNull(toDateTime('2024-07-16 00:00:00'))), toStartOfDay(assumeNotNull(toDateTime('2024-07-23 23:59:59'))))), 1))) AS date, + arrayMap(_match_date -> arraySum(arraySlice(groupArray(count), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> equals(x, _match_date), _days_for_count), _index), 1))), date) AS total +FROM + (SELECT + sum(total) AS count, + day_start + FROM + (SELECT + counts AS total, + toStartOfDay(timestamp) AS day_start + FROM + (SELECT + d.timestamp, + count(DISTINCT actor_id) AS counts + FROM + (SELECT + minus(toStartOfDay(assumeNotNull(toDateTime('2024-07-23 23:59:59'))), toIntervalDay(number)) AS timestamp + FROM + numbers(dateDiff('day', minus(toStartOfDay(assumeNotNull(toDateTime('2024-07-16 00:00:00'))), toIntervalDay(7)), assumeNotNull(toDateTime('2024-07-23 23:59:59')))) AS numbers) AS d + CROSS JOIN (SELECT + timestamp AS timestamp, + e.person_id AS actor_id + FROM + events AS e SAMPLE 1 + WHERE + and(equals(event, '$pageview'), greaterOrEquals(timestamp, minus(assumeNotNull(toDateTime('2024-07-16 00:00:00')), toIntervalDay(7))), lessOrEquals(timestamp, assumeNotNull(toDateTime('2024-07-23 23:59:59')))) + GROUP BY + timestamp, + actor_id) AS e + WHERE + and(lessOrEquals(e.timestamp, plus(d.timestamp, toIntervalDay(1))), greater(e.timestamp, minus(d.timestamp, toIntervalDay(6)))) + GROUP BY + d.timestamp + ORDER BY + d.timestamp ASC) + WHERE + and(greaterOrEquals(timestamp, toStartOfDay(assumeNotNull(toDateTime('2024-07-16 00:00:00')))), lessOrEquals(timestamp, assumeNotNull(toDateTime('2024-07-23 23:59:59'))))) + GROUP BY + day_start + ORDER BY + day_start ASC) +ORDER BY + arraySum(total) DESC +LIMIT 50000 diff --git a/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx b/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx index 3c5d5636e29d18..9a0038556dea6e 100644 --- a/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx +++ b/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx @@ -39,6 +39,7 @@ import { isLifecycleQuery, isPathsQuery, isTrendsQuery, + isValidBreakdown, } from '~/queries/utils' import { AnyPropertyFilter, @@ -155,11 +156,7 @@ function SeriesDisplay({ const { mathDefinitions } = useValues(mathsLogic) const filter = query.series[seriesIndex] - const hasBreakdown = - isInsightQueryWithBreakdown(query) && - query.breakdownFilter != null && - query.breakdownFilter.breakdown_type != null && - query.breakdownFilter.breakdown != null + const hasBreakdown = isInsightQueryWithBreakdown(query) && isValidBreakdown(query.breakdownFilter) const mathDefinition = mathDefinitions[ isLifecycleQuery(query) @@ -338,25 +335,26 @@ export function LEGACY_FilterBasedBreakdownSummary({ filters }: { filters: Parti } export function BreakdownSummary({ query }: { query: InsightQueryNode }): JSX.Element | null { - if ( - !isInsightQueryWithBreakdown(query) || - !query.breakdownFilter || - query.breakdownFilter.breakdown_type == null || - query.breakdownFilter.breakdown == null - ) { + if (!isInsightQueryWithBreakdown(query) || !isValidBreakdown(query.breakdownFilter)) { return null } - const { breakdown_type, breakdown } = query.breakdownFilter - const breakdownArray = Array.isArray(breakdown) ? breakdown : [breakdown] + const { breakdown_type, breakdown, breakdowns } = query.breakdownFilter return ( <>
Breakdown by
- {breakdownArray.map((b) => ( - - ))} + {Array.isArray(breakdowns) + ? breakdowns.map((b) => ( + + )) + : breakdown && + (Array.isArray(breakdown) + ? breakdown + : [breakdown].map((b) => ( + + )))}
) diff --git a/frontend/src/queries/nodes/InsightViz/InsightDisplayConfig.tsx b/frontend/src/queries/nodes/InsightViz/InsightDisplayConfig.tsx index aeca448a782115..86d42ae2ea769b 100644 --- a/frontend/src/queries/nodes/InsightViz/InsightDisplayConfig.tsx +++ b/frontend/src/queries/nodes/InsightViz/InsightDisplayConfig.tsx @@ -28,6 +28,7 @@ import { PathStepPicker } from 'scenes/insights/views/Paths/PathStepPicker' import { trendsDataLogic } from 'scenes/trends/trendsDataLogic' import { useDebouncedCallback } from 'use-debounce' +import { isValidBreakdown } from '~/queries/utils' import { ChartDisplayType } from '~/types' export function InsightDisplayConfig(): JSX.Element { @@ -62,7 +63,7 @@ export function InsightDisplayConfig(): JSX.Element { isLifecycle || ((isTrends || isStickiness) && !(display && NON_TIME_SERIES_DISPLAY_TYPES.includes(display))) const showSmoothing = - isTrends && !breakdownFilter?.breakdown_type && (!display || display === ChartDisplayType.ActionsLineGraph) + isTrends && !isValidBreakdown(breakdownFilter) && (!display || display === ChartDisplayType.ActionsLineGraph) const { showValuesOnSeries, mightContainFractionalNumbers } = useValues(trendsDataLogic(insightProps)) diff --git a/frontend/src/queries/utils.ts b/frontend/src/queries/utils.ts index 05e42c680b0e26..3062354c7df91c 100644 --- a/frontend/src/queries/utils.ts +++ b/frontend/src/queries/utils.ts @@ -447,3 +447,14 @@ export function hogql(strings: TemplateStringsArray, ...values: any[]): string { return strings.reduce((acc, str, i) => acc + str + (i < strings.length - 1 ? formatHogQlValue(values[i]) : ''), '') } hogql.identifier = hogQlIdentifier + +/** + * Wether we have a valid `breakdownFilter` or not. + */ +export function isValidBreakdown(breakdownFilter?: BreakdownFilter | null): breakdownFilter is BreakdownFilter { + return !!( + breakdownFilter && + ((breakdownFilter.breakdown && breakdownFilter.breakdown_type) || + (breakdownFilter.breakdowns && breakdownFilter.breakdowns.length > 0)) + ) +} diff --git a/frontend/src/scenes/insights/filters/BreakdownFilter/taxonomicBreakdownFilterLogic.test.ts b/frontend/src/scenes/insights/filters/BreakdownFilter/taxonomicBreakdownFilterLogic.test.ts index 1064db47dd9815..a011a4a4e1eb6e 100644 --- a/frontend/src/scenes/insights/filters/BreakdownFilter/taxonomicBreakdownFilterLogic.test.ts +++ b/frontend/src/scenes/insights/filters/BreakdownFilter/taxonomicBreakdownFilterLogic.test.ts @@ -2,7 +2,7 @@ import { expectLogic } from 'kea-test-utils' import { TaxonomicFilterGroup, TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { initKeaTests } from '~/test/init' -import { InsightLogicProps } from '~/types' +import { ChartDisplayType, InsightLogicProps } from '~/types' import * as breakdownLogic from './taxonomicBreakdownFilterLogic' @@ -138,6 +138,32 @@ describe('taxonomicBreakdownFilterLogic', () => { }) }) + it('resets the map view when adding a next breakdown', async () => { + logic = taxonomicBreakdownFilterLogic({ + insightProps, + breakdownFilter: { + breakdown: '$geoip_country_code', + breakdown_type: 'person', + }, + isTrends: true, + display: ChartDisplayType.WorldMap, + updateBreakdownFilter, + updateDisplay, + }) + logic.mount() + const changedBreakdown = 'c' + const group: TaxonomicFilterGroup = taxonomicGroupFor(TaxonomicFilterGroupType.EventProperties, undefined) + + await expectLogic(logic, () => { + logic.actions.addBreakdown(changedBreakdown, group) + }).toFinishListeners() + + expect(updateBreakdownFilter).toHaveBeenCalledWith({ + breakdown_type: 'event', + breakdown: 'c', + }) + }) + it('sets a limit', async () => { logic = taxonomicBreakdownFilterLogic({ insightProps, @@ -700,6 +726,35 @@ describe('taxonomicBreakdownFilterLogic', () => { expect(updateBreakdownFilter.mock.calls[0][0]).toHaveProperty('breakdowns', undefined) }) + + it('resets the map view when adding a next breakdown', async () => { + const logic = taxonomicBreakdownFilterLogic({ + insightProps, + breakdownFilter: { + breakdowns: [{ property: '$geoip_country_code', type: 'person' }], + }, + isTrends: true, + display: ChartDisplayType.WorldMap, + updateBreakdownFilter, + updateDisplay, + }) + mockFeatureFlag(logic) + logic.mount() + const changedBreakdown = 'c' + const group: TaxonomicFilterGroup = taxonomicGroupFor(TaxonomicFilterGroupType.EventProperties, undefined) + + await expectLogic(logic, () => { + logic.actions.addBreakdown(changedBreakdown, group) + }).toFinishListeners() + + expect(updateBreakdownFilter).toHaveBeenCalledWith({ + breakdowns: [ + { property: '$geoip_country_code', type: 'person' }, + { property: 'c', type: 'event' }, + ], + }) + expect(updateDisplay).toHaveBeenCalledWith(undefined) + }) }) describe('single breakdown to multiple breakdowns', () => { diff --git a/frontend/src/scenes/insights/filters/BreakdownFilter/taxonomicBreakdownFilterLogic.ts b/frontend/src/scenes/insights/filters/BreakdownFilter/taxonomicBreakdownFilterLogic.ts index 291968d794bf9a..d9ea96530c9554 100644 --- a/frontend/src/scenes/insights/filters/BreakdownFilter/taxonomicBreakdownFilterLogic.ts +++ b/frontend/src/scenes/insights/filters/BreakdownFilter/taxonomicBreakdownFilterLogic.ts @@ -277,6 +277,15 @@ export const taxonomicBreakdownFilterLogic = kea([ isUsingSessionAnalysis: [ (s) => [s.series, s.breakdownFilter, s.properties], (series, breakdownFilter, properties) => { - const using_session_breakdown = breakdownFilter?.breakdown_type === 'session' + const using_session_breakdown = + breakdownFilter?.breakdown_type === 'session' || + breakdownFilter?.breakdowns?.find((breakdown) => breakdown.type === 'session') const using_session_math = series?.some((entity) => entity.math === 'unique_session') const using_session_property_math = series?.some((entity) => { // Should be made more generic is we ever add more session properties @@ -598,6 +600,11 @@ const handleQuerySourceUpdateSideEffects = ( mergedUpdate['properties'] = [] } + // Remove breakdown filter if display type is BoldNumber because it is not supported + if (kind === NodeKind.TrendsQuery && maybeChangedDisplay === ChartDisplayType.BoldNumber) { + mergedUpdate['breakdownFilter'] = null + } + // Don't allow minutes on anything other than Trends if ( currentState.kind == NodeKind.TrendsQuery && diff --git a/frontend/src/scenes/saved-insights/activityDescriptions.tsx b/frontend/src/scenes/saved-insights/activityDescriptions.tsx index 94403b56c65e25..cd7668905e5ad0 100644 --- a/frontend/src/scenes/saved-insights/activityDescriptions.tsx +++ b/frontend/src/scenes/saved-insights/activityDescriptions.tsx @@ -21,7 +21,7 @@ import { urls } from 'scenes/urls' import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' import { InsightQueryNode, QuerySchema, TrendsQuery } from '~/queries/schema' -import { isInsightQueryNode } from '~/queries/utils' +import { isInsightQueryNode, isValidBreakdown } from '~/queries/utils' import { FilterType, InsightModel, InsightShortId } from '~/types' const nameOrLinkToInsight = (short_id?: InsightShortId | null, name?: string | null): string | JSX.Element => { @@ -235,6 +235,7 @@ const insightActionsMapping: Record< function summarizeChanges(filtersAfter: Partial): ChangeMapping | null { const query = filtersToQueryNode(filtersAfter) + const trendsQuery = query as TrendsQuery return { description: ['changed query definition'], @@ -242,7 +243,7 @@ function summarizeChanges(filtersAfter: Partial): ChangeMapping | nu
- {(query as TrendsQuery)?.breakdownFilter?.breakdown_type && } + {isValidBreakdown(trendsQuery?.breakdownFilter) && }
), } diff --git a/posthog/hogql_queries/insights/trends/having.sql b/posthog/hogql_queries/insights/trends/having.sql new file mode 100644 index 00000000000000..ed8045610a9a77 --- /dev/null +++ b/posthog/hogql_queries/insights/trends/having.sql @@ -0,0 +1,11 @@ +SELECT + toStartOfDay(min(timestamp)) as day_start, + argMin(ifNull(nullIf(toString(person.properties.email), ''), '$$_posthog_breakdown_null_$$'), timestamp) AS breakdown_value +FROM + events AS e SAMPLE 1 +WHERE + lessOrEquals(timestamp, assumeNotNull(toDateTime('2024-07-23 23:59:59'))) and event = '$pageview' +GROUP BY + person_id +HAVING + equals(properties.$browser, 'Safari')