diff --git a/.eslintrc.js b/.eslintrc.js index 6b38aaed71fd7..c181d5964eefd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -122,6 +122,22 @@ module.exports = { importNames: ['Alert'], message: 'Please use LemonBanner from @posthog/lemon-ui instead.', }, + { + name: 'antd', + importNames: ['Row'], + message: + 'use flex utility classes instead, e.g. could be
', + }, + { + name: 'antd', + importNames: ['Col'], + message: 'use flex utility classes instead - most of the time can simply be a plain
', + }, + { + name: 'antd', + importNames: ['Card'], + message: 'use utility classes instead', + }, ], }, ], @@ -141,23 +157,10 @@ module.exports = { 'warn', { forbid: [ - { - element: 'Row', - message: - 'use flex utility classes instead, e.g. could be
', - }, - { - element: 'Col', - message: 'use flex utility classes instead - most of the time can simply be a plain
', - }, { element: 'Divider', message: 'use instead', }, - { - element: 'Card', - message: 'use utility classes instead', - }, { element: 'Button', message: 'use instead', diff --git a/ee/api/test/__snapshots__/test_time_to_see_data.ambr b/ee/api/test/__snapshots__/test_time_to_see_data.ambr index 9458103ac1e18..48bec559c2d19 100644 --- a/ee/api/test/__snapshots__/test_time_to_see_data.ambr +++ b/ee/api/test/__snapshots__/test_time_to_see_data.ambr @@ -17,6 +17,7 @@ "user": { "distinct_id": "", "first_name": "", + "last_name": "", "email": "", "is_email_verified": false } diff --git a/ee/billing/quota_limiting.py b/ee/billing/quota_limiting.py index ef3e12a421575..0809266b1db64 100644 --- a/ee/billing/quota_limiting.py +++ b/ee/billing/quota_limiting.py @@ -147,12 +147,12 @@ def set_org_usage_summary( new_usage = copy.deepcopy(new_usage) for field in ["events", "recordings", "rows_synced"]: - resource_usage = new_usage[field] # type: ignore + resource_usage = new_usage.get(field, {"limit": None, "usage": 0, "todays_usage": 0}) if not resource_usage: continue if todays_usage: - resource_usage["todays_usage"] = todays_usage[field] # type: ignore + resource_usage["todays_usage"] = todays_usage.get(field, 0) else: # TRICKY: If we are not explictly setting todays_usage, we want to reset it to 0 IF the incoming new_usage is different if (organization.usage or {}).get(field, {}).get("usage") != resource_usage.get("usage"): diff --git a/ee/clickhouse/queries/funnels/test/__snapshots__/test_funnel.ambr b/ee/clickhouse/queries/funnels/test/__snapshots__/test_funnel.ambr index bb14426e16441..7786b0efe8d69 100644 --- a/ee/clickhouse/queries/funnels/test/__snapshots__/test_funnel.ambr +++ b/ee/clickhouse/queries/funnels/test/__snapshots__/test_funnel.ambr @@ -19,7 +19,7 @@ AND notEmpty(e.person_id) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -160,7 +160,7 @@ AND notEmpty(e.person_id) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -299,7 +299,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -437,7 +437,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -572,7 +572,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -707,7 +707,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -842,7 +842,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -979,7 +979,7 @@ AND notEmpty(e.person_id) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1120,7 +1120,7 @@ AND notEmpty(e.person_id) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1259,7 +1259,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1397,7 +1397,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1510,7 +1510,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1623,7 +1623,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1736,7 +1736,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1851,7 +1851,7 @@ AND notEmpty(e.person_id) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1992,7 +1992,7 @@ AND notEmpty(e.person_id) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -2131,7 +2131,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -2269,7 +2269,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -2292,7 +2292,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -2315,7 +2315,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -2547,7 +2547,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -2570,7 +2570,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -2593,7 +2593,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -2825,7 +2825,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -2848,7 +2848,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -2871,7 +2871,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -3103,7 +3103,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -3126,7 +3126,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -3149,7 +3149,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- diff --git a/ee/clickhouse/queries/test/__snapshots__/test_breakdown_props.ambr b/ee/clickhouse/queries/test/__snapshots__/test_breakdown_props.ambr index 415ed17869d25..0111a16581c61 100644 --- a/ee/clickhouse/queries/test/__snapshots__/test_breakdown_props.ambr +++ b/ee/clickhouse/queries/test/__snapshots__/test_breakdown_props.ambr @@ -19,7 +19,7 @@ OR NOT JSONHas(group_properties_0, 'out'))) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 5 + LIMIT 6 OFFSET 0 ' --- @@ -44,7 +44,7 @@ OR NOT JSONHas(group_properties_0, 'out'))) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 5 + LIMIT 6 OFFSET 0 ' --- @@ -74,7 +74,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 5 + LIMIT 6 OFFSET 0 ' --- @@ -104,7 +104,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 5 + LIMIT 6 OFFSET 0 ' --- @@ -142,7 +142,7 @@ OR has(['val'], replaceRegexpAll(JSONExtractRaw(e.properties, 'key'), '^"|"$', '')))) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 5 + LIMIT 6 OFFSET 0 ' --- @@ -167,7 +167,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-12 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -206,7 +206,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-12 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -245,7 +245,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-12 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- diff --git a/ee/clickhouse/queries/test/test_breakdown_props.py b/ee/clickhouse/queries/test/test_breakdown_props.py index b937c63fed66f..bd398812bb97a 100644 --- a/ee/clickhouse/queries/test/test_breakdown_props.py +++ b/ee/clickhouse/queries/test/test_breakdown_props.py @@ -94,7 +94,7 @@ def test_breakdown_person_props(self): "count(*)", self.team, ) - self.assertEqual(res, ["test"]) + self.assertEqual(res[0], ["test"]) def test_breakdown_person_props_with_entity_filter(self): _create_person(team_id=self.team.pk, distinct_ids=["p1"], properties={"$browser": "test"}) @@ -150,7 +150,7 @@ def test_breakdown_person_props_with_entity_filter(self): } ) res = get_breakdown_prop_values(filter, Entity(entity_params[0]), "count(*)", self.team) - self.assertEqual(res, ["test"]) + self.assertEqual(res[0], ["test"]) @snapshot_clickhouse_queries def test_breakdown_person_props_with_entity_filter_and_or_props_with_partial_pushdown(self): @@ -242,7 +242,7 @@ def test_breakdown_person_props_with_entity_filter_and_or_props_with_partial_pus "funnel_window_days": 14, } ) - res = sorted(get_breakdown_prop_values(filter, Entity(entity_params[0]), "count(*)", self.team)) + res = sorted(get_breakdown_prop_values(filter, Entity(entity_params[0]), "count(*)", self.team)[0]) self.assertEqual(res, ["test", "test2"]) @snapshot_clickhouse_queries @@ -319,7 +319,7 @@ def test_breakdown_group_props(self): team=self.team, ) result = get_breakdown_prop_values(filter, filter.entities[0], "count(*)", self.team) - self.assertEqual(result, ["finance", "technology"]) + self.assertEqual(result[0], ["finance", "technology"]) filter = Filter( data={ @@ -345,7 +345,7 @@ def test_breakdown_group_props(self): } ) result = get_breakdown_prop_values(filter, filter.entities[0], "count(*)", self.team) - self.assertEqual(result, ["finance", "technology"]) + self.assertEqual(result[0], ["finance", "technology"]) @snapshot_clickhouse_queries def test_breakdown_session_props(self): @@ -397,7 +397,7 @@ def test_breakdown_session_props(self): } ) result = get_breakdown_prop_values(filter, filter.entities[0], "count(*)", self.team) - self.assertEqual(result, [70, 20]) + self.assertEqual(result[0], [70, 20]) @snapshot_clickhouse_queries def test_breakdown_with_math_property_session(self): @@ -511,10 +511,10 @@ def test_breakdown_with_math_property_session(self): result = get_breakdown_prop_values(filter, filter.entities[0], aggregate_operation, self.team) # test should come first, based on aggregate operation, even if absolute count of events for # mac is higher - self.assertEqual(result, ["test", "mac"]) + self.assertEqual(result[0], ["test", "mac"]) result = get_breakdown_prop_values(filter, filter.entities[0], "count(*)", self.team) - self.assertEqual(result, ["mac", "test"]) + self.assertEqual(result[0], ["mac", "test"]) @pytest.mark.parametrize( diff --git a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr index 5fa656c60136d..0e07c15554bae 100644 --- a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr +++ b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr @@ -22,7 +22,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-06 00:00:00', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -83,7 +83,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-06 00:00:00', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- diff --git a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiments.ambr b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiments.ambr index 9738ec2e3a988..72883735513ab 100644 --- a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiments.ambr +++ b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiments.ambr @@ -10,7 +10,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-06 00:00:00', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -212,7 +212,7 @@ AND toTimeZone(timestamp, 'Europe/Amsterdam') <= toDateTime('2020-01-06 10:00:00', 'Europe/Amsterdam') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -414,7 +414,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-06 00:00:00', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -616,7 +616,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-06 00:00:00', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -819,7 +819,7 @@ AND (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -883,7 +883,7 @@ AND has(['a-b-test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag'), '^"|"$', ''))) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1111,7 +1111,7 @@ AND (has(['control', 'test_1', 'test_2', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1175,7 +1175,7 @@ AND has(['a-b-test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag'), '^"|"$', ''))) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1296,7 +1296,7 @@ AND (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1360,7 +1360,7 @@ AND has(['a-b-test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag'), '^"|"$', ''))) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1589,7 +1589,7 @@ AND (ifNull(ilike(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'hogql'), ''), 'null'), '^"|"$', ''), 'true'), 0))) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1654,7 +1654,7 @@ AND has(['a-b-test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag'), '^"|"$', ''))) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- diff --git a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_trends.ambr b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_trends.ambr index 3b4b040a8ebd5..5bdb57e5693ef 100644 --- a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_trends.ambr +++ b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_trends.ambr @@ -225,7 +225,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -398,7 +398,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -511,7 +511,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- diff --git a/frontend/__snapshots__/components-cards-insight-card--insight-card.png b/frontend/__snapshots__/components-cards-insight-card--insight-card.png new file mode 100644 index 0000000000000..30865d9b1686f Binary files /dev/null and b/frontend/__snapshots__/components-cards-insight-card--insight-card.png differ diff --git a/frontend/__snapshots__/components-properties-table--properties-table.png b/frontend/__snapshots__/components-properties-table--properties-table.png new file mode 100644 index 0000000000000..d2f6a5e61cc90 Binary files /dev/null and b/frontend/__snapshots__/components-properties-table--properties-table.png differ diff --git a/frontend/__snapshots__/filters-taxonomic-filter--properties.png b/frontend/__snapshots__/filters-taxonomic-filter--properties.png new file mode 100644 index 0000000000000..b5e28467d4779 Binary files /dev/null and b/frontend/__snapshots__/filters-taxonomic-filter--properties.png differ diff --git a/frontend/__snapshots__/scenes-app-feature-flags-code-examples--code-instructionsi-os-with-multivariate-flag.png b/frontend/__snapshots__/scenes-app-feature-flags-code-examples--code-instructionsi-os-with-multivariate-flag.png new file mode 100644 index 0000000000000..e11a055812aab Binary files /dev/null and b/frontend/__snapshots__/scenes-app-feature-flags-code-examples--code-instructionsi-os-with-multivariate-flag.png differ diff --git a/frontend/__snapshots__/scenes-app-saved-insights--card-view.png b/frontend/__snapshots__/scenes-app-saved-insights--card-view.png new file mode 100644 index 0000000000000..3804a724032bf Binary files /dev/null and b/frontend/__snapshots__/scenes-app-saved-insights--card-view.png differ diff --git a/frontend/src/layout/navigation-3000/components/MinimalNavigation.tsx b/frontend/src/layout/navigation-3000/components/MinimalNavigation.tsx index 1161a4dadc1c8..cf0e586b6192a 100644 --- a/frontend/src/layout/navigation-3000/components/MinimalNavigation.tsx +++ b/frontend/src/layout/navigation-3000/components/MinimalNavigation.tsx @@ -51,7 +51,7 @@ export function MinimalNavigation(): JSX.Element { } + icon={} onClick={toggleSitePopover} > {user?.first_name || user?.email} diff --git a/frontend/src/layout/navigation-3000/components/Navbar.tsx b/frontend/src/layout/navigation-3000/components/Navbar.tsx index 3ca2e1690fe4f..6fbae5671a9c3 100644 --- a/frontend/src/layout/navigation-3000/components/Navbar.tsx +++ b/frontend/src/layout/navigation-3000/components/Navbar.tsx @@ -107,7 +107,7 @@ export function Navbar(): JSX.Element { className="min-w-70" > } + icon={} identifier="me" title={`Hi${user?.first_name ? `, ${user?.first_name}` : ''}!`} shortTitle={user?.first_name || user?.email} diff --git a/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.tsx b/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.tsx index 2b40103a7454c..ec769563d9777 100644 --- a/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.tsx +++ b/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.tsx @@ -114,7 +114,7 @@ export const breadcrumbsLogic = kea([ breadcrumbs.push({ key: 'me', name: user.first_name, - symbol: , + symbol: , }) } // Instance diff --git a/frontend/src/layout/navigation/TopBar/SitePopover.tsx b/frontend/src/layout/navigation/TopBar/SitePopover.tsx index 937e37dcef90f..e4455a04f1087 100644 --- a/frontend/src/layout/navigation/TopBar/SitePopover.tsx +++ b/frontend/src/layout/navigation/TopBar/SitePopover.tsx @@ -72,7 +72,7 @@ function AccountInfo(): JSX.Element { tooltipPlacement="left" sideIcon={} > - +
{user?.first_name}
@@ -316,7 +316,7 @@ export function SitePopover(): JSX.Element { >
- + {!systemStatusHealthy && }
diff --git a/frontend/src/lib/components/ActivityLog/ActivityLog.tsx b/frontend/src/lib/components/ActivityLog/ActivityLog.tsx index 52834f7479876..fe00761d2fa49 100644 --- a/frontend/src/lib/components/ActivityLog/ActivityLog.tsx +++ b/frontend/src/lib/components/ActivityLog/ActivityLog.tsx @@ -73,9 +73,11 @@ export const ActivityLogRow = ({
diff --git a/frontend/src/lib/components/ActivityLog/humanizeActivity.tsx b/frontend/src/lib/components/ActivityLog/humanizeActivity.tsx index e1dc145a86a8b..28bdb6b4aa784 100644 --- a/frontend/src/lib/components/ActivityLog/humanizeActivity.tsx +++ b/frontend/src/lib/components/ActivityLog/humanizeActivity.tsx @@ -1,4 +1,5 @@ import { dayjs } from 'lib/dayjs' +import { fullName } from 'lib/utils' import { InsightShortId, PersonType } from '~/types' @@ -110,7 +111,7 @@ export function humanize( if (description !== null) { logLines.push({ email: logItem.user?.email, - name: logItem.user?.first_name, + name: logItem.user ? fullName(logItem.user) : undefined, isSystem: logItem.is_system, description, extendedDescription, @@ -126,5 +127,5 @@ export function userNameForLogItem(logItem: ActivityLogItem): string { if (logItem.is_system) { return 'PostHog' } - return logItem.user?.first_name ?? 'A user' + return logItem.user ? fullName(logItem.user) : 'A user' } diff --git a/frontend/src/lib/components/AnnotationsOverlay/AnnotationsOverlay.tsx b/frontend/src/lib/components/AnnotationsOverlay/AnnotationsOverlay.tsx index 3f43c79ca46d0..dd8731af9c5b4 100644 --- a/frontend/src/lib/components/AnnotationsOverlay/AnnotationsOverlay.tsx +++ b/frontend/src/lib/components/AnnotationsOverlay/AnnotationsOverlay.tsx @@ -258,8 +258,9 @@ function AnnotationCard({ annotation }: { annotation: AnnotationType }): JSX.Ele
{annotation.content}
Created by
- {' '} - +
Last modified by
- {' '} + {' '}
diff --git a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx index bfaf77e7f7be8..c731268a610a2 100644 --- a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx +++ b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx @@ -648,9 +648,7 @@ export const commandPaletteLogic = kea([ }, }, { - icon: () => ( - - ), + icon: () => , display: 'Go to User settings', synonyms: ['account', 'profile'], executor: () => { diff --git a/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx b/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx index 118327650e662..bd250bfe90adb 100644 --- a/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx +++ b/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx @@ -185,7 +185,7 @@ function Owner({ user }: { user?: UserBasicType | null }): JSX.Element { <> {user?.uuid ? (
- + {user.first_name}
) : ( diff --git a/frontend/src/lib/components/MemberSelect.tsx b/frontend/src/lib/components/MemberSelect.tsx index a1cb6e1899c92..3ab1d0a793662 100644 --- a/frontend/src/lib/components/MemberSelect.tsx +++ b/frontend/src/lib/components/MemberSelect.tsx @@ -1,5 +1,6 @@ import { LemonButton, LemonButtonProps, LemonDropdown, LemonInput, ProfilePicture } from '@posthog/lemon-ui' import { useValues } from 'kea' +import { fullName } from 'lib/utils' import { useMemo, useState } from 'react' import { membersLogic } from 'scenes/organization/membersLogic' @@ -79,17 +80,11 @@ export function MemberSelect({ fullWidth role="menuitem" size="small" - icon={ - - } + icon={} onClick={() => _onChange(member.user)} > - {member.user.first_name} + {fullName(member.user)} {meFirstMembers[0] === member && `(you)`} @@ -112,7 +107,7 @@ export function MemberSelect({ selectedMember ) : selectedMember ? ( - {selectedMember.first_name} + {fullName(selectedMember)} {meFirstMembers[0].user.uuid === selectedMember.uuid ? ` (you)` : ''} ) : ( diff --git a/frontend/src/lib/components/Table/Table.tsx b/frontend/src/lib/components/Table/Table.tsx index 7cf69790d2763..afcecc0eb15c9 100644 --- a/frontend/src/lib/components/Table/Table.tsx +++ b/frontend/src/lib/components/Table/Table.tsx @@ -31,9 +31,7 @@ export function createdByColumn = Record - {item.created_by && ( - - )} + {item.created_by && } {/* eslint-disable-next-line react/forbid-dom-props */}
{item.created_by ? item.created_by.first_name || item.created_by.email : '-'} diff --git a/frontend/src/lib/components/UserActivityIndicator/UserActivityIndicator.tsx b/frontend/src/lib/components/UserActivityIndicator/UserActivityIndicator.tsx index cd5f5cd1e2d70..8f8e602b6103f 100644 --- a/frontend/src/lib/components/UserActivityIndicator/UserActivityIndicator.tsx +++ b/frontend/src/lib/components/UserActivityIndicator/UserActivityIndicator.tsx @@ -27,7 +27,7 @@ export function UserActivityIndicator({ {at && } {by && by}
- {by && } + {by && }
) : null } diff --git a/frontend/src/lib/components/UserSelectItem.tsx b/frontend/src/lib/components/UserSelectItem.tsx index 0732418c35479..7990f4c8a7301 100644 --- a/frontend/src/lib/components/UserSelectItem.tsx +++ b/frontend/src/lib/components/UserSelectItem.tsx @@ -10,7 +10,7 @@ export interface UserSelectItemProps { export function UserSelectItem({ user }: UserSelectItemProps): JSX.Element { return ( - + {user.first_name} {`<${user.email}>`} diff --git a/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.stories.tsx b/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.stories.tsx index c360b9f86e796..baa2f805f48e4 100644 --- a/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.stories.tsx @@ -16,7 +16,13 @@ const meta: Meta = { [`user-${i}`]: { labelComponent: ( - + {capitalizeFirstLetter(x)} {`<${x}@posthog.com>`} diff --git a/frontend/src/lib/lemon-ui/LemonSnack/LemonSnack.stories.tsx b/frontend/src/lib/lemon-ui/LemonSnack/LemonSnack.stories.tsx index e7e2c9528687d..cffe497f947bb 100644 --- a/frontend/src/lib/lemon-ui/LemonSnack/LemonSnack.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonSnack/LemonSnack.stories.tsx @@ -44,7 +44,7 @@ export const ComplexContent: Story = BasicTemplate.bind({}) ComplexContent.args = { children: ( - + Look at me I'm bold! diff --git a/frontend/src/lib/lemon-ui/LemonTable/columnUtils.tsx b/frontend/src/lib/lemon-ui/LemonTable/columnUtils.tsx index 935dc5500b9ef..b258ba98de193 100644 --- a/frontend/src/lib/lemon-ui/LemonTable/columnUtils.tsx +++ b/frontend/src/lib/lemon-ui/LemonTable/columnUtils.tsx @@ -39,9 +39,7 @@ export function createdByColumn const { created_by } = item return (
- {created_by && ( - - )} + {created_by && }
) }, diff --git a/frontend/src/lib/lemon-ui/ProfilePicture/ProfileBubbles.tsx b/frontend/src/lib/lemon-ui/ProfilePicture/ProfileBubbles.tsx index 23cd6b167efeb..0da16a220f548 100644 --- a/frontend/src/lib/lemon-ui/ProfilePicture/ProfileBubbles.tsx +++ b/frontend/src/lib/lemon-ui/ProfilePicture/ProfileBubbles.tsx @@ -29,8 +29,10 @@ export function ProfileBubbles({ people, tooltip, limit = 6, ...divProps }: Prof {shownPeople.map(({ email, name, title }, index) => ( , 'first_name' | 'email' | 'last_name'> | null name?: string - email?: string size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' showName?: boolean className?: string @@ -22,8 +24,8 @@ export interface ProfilePictureProps { } export function ProfilePicture({ + user, name, - email, size = 'lg', showName, className, @@ -31,9 +33,16 @@ export function ProfilePicture({ title, type = 'person', }: ProfilePictureProps): JSX.Element { - const { user } = useValues(userLogic) + const { user: currentUser } = useValues(userLogic) const [gravatarLoaded, setGravatarLoaded] = useState() + let email = user?.email + + if (user) { + name = fullName(user) + email = user.email + } + const combinedNameAndEmail = name && email ? `${name} <${email}>` : name || email const gravatarUrl = useMemo(() => { @@ -82,7 +91,9 @@ export function ProfilePicture({ ) : (
{pictureComponent} - {user?.email === email ? 'you' : name || email || 'an unknown user'} + + {currentUser?.email === email ? 'you' : name || email || 'an unknown user'} +
) } diff --git a/frontend/src/lib/taxonomy.tsx b/frontend/src/lib/taxonomy.tsx index 11eed4557dff1..a0219ddd5f1ab 100644 --- a/frontend/src/lib/taxonomy.tsx +++ b/frontend/src/lib/taxonomy.tsx @@ -807,6 +807,76 @@ export const KEY_MAPPING: KeyMappingInterface = { description: 'UTM Source. (First-touch, session-scoped)', examples: ['free goodies'], }, + // Mobile SDKs events + 'Application Opened': { + label: 'Application Opened', + description: 'When a user opens the app either for the first time or from the foreground.', + }, + 'Application Backgrounded': { + label: 'Application Backgrounded', + description: 'When a user puts the app in the background.', + }, + 'Application Updated': { + label: 'Application Updated', + description: 'When a user upgrades the app.', + }, + 'Application Installed': { + label: 'Application Installed', + description: 'When a user installs the app.', + }, + 'Application Became Active': { + label: 'Application Became Active', + description: 'When a user puts the app in the foreground.', + }, + 'Deep Link Opened': { + label: 'Deep Link Opened', + description: 'When a user opens the app via a deep link.', + }, + $network_carrier: { + label: 'Network Carrier', + description: 'The network carrier that the user is on.', + examples: ['cricket', 'telecom'], + }, + // set by the Application Opened event + from_background: { + label: 'From Background', + description: 'Whether the app was opened for the first time or from the background.', + examples: ['true', 'false'], + }, + // set by the Application Opened/Deep Link Opened event + url: { + label: 'URL', + description: 'The deep link URL that the app was opened from.', + examples: ['https://open.my.app'], + }, + referring_application: { + label: 'Referrer Application', + description: 'The namespace of the app that made the request.', + examples: ['com.posthog.app'], + }, + // set by the Application Installed/Application Updated/Application Opened events + // similar to $app_version + version: { + label: 'App Version', + description: 'The version of the app', + examples: ['1.0.0'], + }, + previous_version: { + label: 'App Previous Version', + description: 'The previous version of the app', + examples: ['1.0.0'], + }, + // similar to $app_build + build: { + label: 'App Build', + description: 'The build number for the app', + examples: ['1'], + }, + previous_build: { + label: 'App Previous Build', + description: 'The previous build number for the app', + examples: ['1'], + }, }, element: { tag_name: { diff --git a/frontend/src/lib/utils.tsx b/frontend/src/lib/utils.tsx index 4477d4d3ab553..7e73d58fc2d0f 100644 --- a/frontend/src/lib/utils.tsx +++ b/frontend/src/lib/utils.tsx @@ -191,6 +191,10 @@ export function capitalizeFirstLetter(string: string): string { return string.charAt(0).toUpperCase() + string.slice(1) } +export function fullName(props: { first_name?: string; last_name?: string }): string { + return `${props.first_name || ''} ${props.last_name || ''}`.trim() +} + export const genericOperatorMap: Record = { exact: '= equals', is_not: "≠ doesn't equal", diff --git a/frontend/src/queries/nodes/DataNode/LoadNext.tsx b/frontend/src/queries/nodes/DataNode/LoadNext.tsx index 714d391ce4ba7..9bd1280e083bf 100644 --- a/frontend/src/queries/nodes/DataNode/LoadNext.tsx +++ b/frontend/src/queries/nodes/DataNode/LoadNext.tsx @@ -3,7 +3,7 @@ import { LemonButton } from 'lib/lemon-ui/LemonButton' import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { DataNode } from '~/queries/schema' -import { isPersonsNode, isPersonsQuery } from '~/queries/utils' +import { isActorsQuery, isPersonsNode } from '~/queries/utils' interface LoadNextProps { query: DataNode @@ -17,7 +17,7 @@ export function LoadNext({ query }: LoadNextProps): JSX.Element { Showing {canLoadNextData || numberOfRows === 1 ? '' : 'all '} {numberOfRows === 1 ? 'one' : numberOfRows}{' '} - {isPersonsNode(query) || isPersonsQuery(query) + {isPersonsNode(query) || isActorsQuery(query) ? numberOfRows === 1 ? 'person' : 'people' diff --git a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts index c733efa7e3e75..1196a143c1de2 100644 --- a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts +++ b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts @@ -29,6 +29,8 @@ import { userLogic } from 'scenes/userLogic' import { removeExpressionComment } from '~/queries/nodes/DataTable/utils' import { query } from '~/queries/query' import { + ActorsQuery, + ActorsQueryResponse, AnyResponseType, DataNode, EventsQuery, @@ -36,18 +38,16 @@ import { InsightVizNode, NodeKind, PersonsNode, - PersonsQuery, - PersonsQueryResponse, QueryResponse, QueryTiming, } from '~/queries/schema' import { + isActorsQuery, isEventsQuery, - isInsightPersonsQuery, + isInsightActorsQuery, isInsightQueryNode, isLifecycleQuery, isPersonsNode, - isPersonsQuery, isTrendsQuery, } from '~/queries/utils' @@ -212,7 +212,7 @@ export const dataNodeLogic = kea([ } // TODO: unify when we use the same backend endpoint for both const now = performance.now() - if (isEventsQuery(props.query) || isPersonsQuery(props.query)) { + if (isEventsQuery(props.query) || isActorsQuery(props.query)) { const newResponse = (await query(values.nextQuery)) ?? null actions.setElapsedTime(performance.now() - now) const queryResponse = values.response as QueryResponse @@ -396,8 +396,8 @@ export const dataNodeLogic = kea([ return null } - if ((isEventsQuery(query) || isPersonsQuery(query)) && !responseError && !dataLoading) { - if ((response as EventsQueryResponse | PersonsQueryResponse)?.hasMore) { + if ((isEventsQuery(query) || isActorsQuery(query)) && !responseError && !dataLoading) { + if ((response as EventsQueryResponse | ActorsQueryResponse)?.hasMore) { const sortKey = query.orderBy?.[0] ?? 'timestamp DESC' const typedResults = (response as QueryResponse)?.results if (isEventsQuery(query) && sortKey === 'timestamp DESC') { @@ -423,7 +423,7 @@ export const dataNodeLogic = kea([ ...query, offset: typedResults?.length || 0, limit: Math.max(100, Math.min(2 * (typedResults?.length || 100), LOAD_MORE_ROWS_LIMIT)), - } as EventsQuery | PersonsQuery + } as EventsQuery | ActorsQuery } } } @@ -446,7 +446,7 @@ export const dataNodeLogic = kea([ backToSourceQuery: [ (s) => [s.query], (query): InsightVizNode | null => { - if (isPersonsQuery(query) && isInsightPersonsQuery(query.source) && !!query.source.source) { + if (isActorsQuery(query) && isInsightActorsQuery(query.source) && !!query.source.source) { const insightQuery = query.source.source const insightVizNode: InsightVizNode = { kind: NodeKind.InsightVizNode, diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 7cfec3e91a888..b0a3c52382acb 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -41,20 +41,20 @@ import { OpenEditorButton } from '~/queries/nodes/Node/OpenEditorButton' import { PersonPropertyFilters } from '~/queries/nodes/PersonsNode/PersonPropertyFilters' import { PersonsSearch } from '~/queries/nodes/PersonsNode/PersonsSearch' import { + ActorsQuery, AnyResponseType, DataTableNode, EventsNode, EventsQuery, HogQLQuery, PersonsNode, - PersonsQuery, } from '~/queries/schema' import { QueryContext } from '~/queries/types' import { + isActorsQuery, isEventsQuery, isHogQlAggregation, isHogQLQuery, - isPersonsQuery, taxonomicEventFilterToHogQL, taxonomicPersonFilterToHogQL, } from '~/queries/utils' @@ -138,8 +138,8 @@ export function DataTable({ uniqueKey, query, setQuery, context, cachedResults } ? columnsInResponse ?? columnsInQuery : columnsInQuery - const groupTypes = isPersonsQuery(query.source) ? personGroupTypes : eventGroupTypes - const hogQLTable = isPersonsQuery(query.source) ? 'persons' : 'events' + const groupTypes = isActorsQuery(query.source) ? personGroupTypes : eventGroupTypes + const hogQLTable = isActorsQuery(query.source) ? 'persons' : 'events' const lemonColumns: LemonTableColumn[] = [ ...columnsInLemonTable.map((key, index) => ({ @@ -182,7 +182,7 @@ export function DataTable({ uniqueKey, query, setQuery, context, cachedResults } type="tertiary" fullWidth onChange={(v, g) => { - const hogQl = isPersonsQuery(query.source) + const hogQl = isActorsQuery(query.source) ? taxonomicPersonFilterToHogQL(g, v) : taxonomicEventFilterToHogQL(g, v) if (setQuery && hogQl && sourceFeatures.has(QueryFeature.selectAndOrderByColumns)) { @@ -263,7 +263,7 @@ export function DataTable({ uniqueKey, query, setQuery, context, cachedResults } type="tertiary" fullWidth onChange={(v, g) => { - const hogQl = isPersonsQuery(query.source) + const hogQl = isActorsQuery(query.source) ? taxonomicPersonFilterToHogQL(g, v) : taxonomicEventFilterToHogQL(g, v) if (setQuery && hogQl && sourceFeatures.has(QueryFeature.selectAndOrderByColumns)) { @@ -277,7 +277,7 @@ export function DataTable({ uniqueKey, query, setQuery, context, cachedResults } select: [...columns.slice(0, index), hogQl, ...columns.slice(index)].filter( (c) => (isAggregation ? c !== '*' && c !== 'person.$delete' : true) ), - } as EventsQuery | PersonsQuery, + } as EventsQuery | ActorsQuery, }) } }} @@ -292,7 +292,7 @@ export function DataTable({ uniqueKey, query, setQuery, context, cachedResults } type="tertiary" fullWidth onChange={(v, g) => { - const hogQl = isPersonsQuery(query.source) + const hogQl = isActorsQuery(query.source) ? taxonomicPersonFilterToHogQL(g, v) : taxonomicEventFilterToHogQL(g, v) if (setQuery && hogQl && sourceFeatures.has(QueryFeature.selectAndOrderByColumns)) { @@ -310,7 +310,7 @@ export function DataTable({ uniqueKey, query, setQuery, context, cachedResults } ].filter((c) => isAggregation ? c !== '*' && c !== 'person.$delete' : true ), - } as EventsQuery | PersonsQuery, + } as EventsQuery | ActorsQuery, }) } }} @@ -370,8 +370,7 @@ export function DataTable({ uniqueKey, query, setQuery, context, cachedResults } ].filter((column) => !query.hiddenColumns?.includes(column.dataIndex) && column.dataIndex !== '*') const setQuerySource = useCallback( - (source: EventsNode | EventsQuery | PersonsNode | PersonsQuery | HogQLQuery) => - setQuery?.({ ...query, source }), + (source: EventsNode | EventsQuery | PersonsNode | ActorsQuery | HogQLQuery) => setQuery?.({ ...query, source }), [setQuery] ) diff --git a/frontend/src/queries/nodes/DataTable/queryFeatures.ts b/frontend/src/queries/nodes/DataTable/queryFeatures.ts index 4c2b4202ea539..560ee7424013a 100644 --- a/frontend/src/queries/nodes/DataTable/queryFeatures.ts +++ b/frontend/src/queries/nodes/DataTable/queryFeatures.ts @@ -1,9 +1,9 @@ import { Node } from '~/queries/schema' import { + isActorsQuery, isEventsQuery, isHogQLQuery, isPersonsNode, - isPersonsQuery, isWebOverviewQuery, isWebStatsTableQuery, isWebTopClicksQuery, @@ -43,11 +43,11 @@ export function getQueryFeatures(query: Node): Set { features.add(QueryFeature.selectAndOrderByColumns) } - if (isPersonsNode(query) || isPersonsQuery(query)) { + if (isPersonsNode(query) || isActorsQuery(query)) { features.add(QueryFeature.personPropertyFilters) features.add(QueryFeature.personsSearch) - if (isPersonsQuery(query)) { + if (isActorsQuery(query)) { features.add(QueryFeature.selectAndOrderByColumns) features.add(QueryFeature.columnsInResponse) features.add(QueryFeature.resultIsArrayOfArrays) diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx index ac141f208630c..9079149cc2bfc 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumn.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx @@ -18,10 +18,10 @@ import { DeletePersonButton } from '~/queries/nodes/PersonsNode/DeletePersonButt import { DataTableNode, EventsQueryPersonColumn, HasPropertiesNode } from '~/queries/schema' import { QueryContext } from '~/queries/types' import { + isActorsQuery, isEventsQuery, isHogQLQuery, isPersonsNode, - isPersonsQuery, isTimeToSeeDataSessionsQuery, trimQuotes, } from '~/queries/utils' @@ -221,7 +221,7 @@ export function renderColumn( displayProps.href = urls.personByDistinctId(personRecord.distinct_ids[0]) } - if (isPersonsQuery(query.source) && value) { + if (isActorsQuery(query.source) && value) { displayProps.person = value displayProps.href = value.id ? urls.personByUUID(value.id) @@ -229,14 +229,14 @@ export function renderColumn( } return - } else if (key === 'person.$delete' && (isPersonsNode(query.source) || isPersonsQuery(query.source))) { + } else if (key === 'person.$delete' && (isPersonsNode(query.source) || isActorsQuery(query.source))) { const personRecord = record as PersonType return } else if (key.startsWith('context.columns.')) { const columnName = trimQuotes(key.substring(16)) // 16 = "context.columns.".length const Component = context?.columns?.[columnName]?.render return Component ? : '' - } else if (key === 'id' && (isPersonsNode(query.source) || isPersonsQuery(query.source))) { + } else if (key === 'id' && (isPersonsNode(query.source) || isActorsQuery(query.source))) { return ( void + query: PersonsNode | ActorsQuery + setQuery?: (query: PersonsNode | ActorsQuery) => void } let uniqueNode = 0 @@ -25,7 +25,7 @@ export function PersonPropertyFilters({ query, setQuery }: PersonPropertyFilters }} pageKey={`PersonPropertyFilters.${id}`} taxonomicGroupTypes={ - isPersonsQuery(query) + isActorsQuery(query) ? [ TaxonomicFilterGroupType.PersonProperties, TaxonomicFilterGroupType.Cohorts, diff --git a/frontend/src/queries/nodes/PersonsNode/PersonsSearch.tsx b/frontend/src/queries/nodes/PersonsNode/PersonsSearch.tsx index 8836b76cac7de..2dc4c7adece92 100644 --- a/frontend/src/queries/nodes/PersonsNode/PersonsSearch.tsx +++ b/frontend/src/queries/nodes/PersonsNode/PersonsSearch.tsx @@ -3,13 +3,13 @@ import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { useDebouncedQuery } from '~/queries/hooks/useDebouncedQuery' -import { PersonsNode, PersonsQuery } from '~/queries/schema' +import { ActorsQuery, PersonsNode } from '~/queries/schema' import { isQueryForGroup } from '~/queries/utils' type ActorType = 'person' | 'group' interface PersonSearchProps { - query: PersonsNode | PersonsQuery - setQuery?: (query: PersonsNode | PersonsQuery) => void + query: PersonsNode | ActorsQuery + setQuery?: (query: PersonsNode | ActorsQuery) => void } interface LabelType { @@ -31,7 +31,7 @@ const labels: Record = { } export function PersonsSearch({ query, setQuery }: PersonSearchProps): JSX.Element { - const { value, onChange } = useDebouncedQuery( + const { value, onChange } = useDebouncedQuery( query, setQuery, (query) => query.search || '', diff --git a/frontend/src/queries/nodes/TimeToSeeData/Trace/traceLogic.tsx b/frontend/src/queries/nodes/TimeToSeeData/Trace/traceLogic.tsx index c36416b2fe033..232748290d242 100644 --- a/frontend/src/queries/nodes/TimeToSeeData/Trace/traceLogic.tsx +++ b/frontend/src/queries/nodes/TimeToSeeData/Trace/traceLogic.tsx @@ -21,7 +21,7 @@ export function sessionNodeFacts(node: TimeToSeeNode): Record, + user: , duration: humanFriendlyMilliseconds(node.data.duration_ms) || 'unknown', sessionEventCount: node.data.events_count, frustratingInteractions: node.data.frustrating_interactions_count, diff --git a/frontend/src/queries/query.ts b/frontend/src/queries/query.ts index 5d9b63e1d151b..348a57059a2c3 100644 --- a/frontend/src/queries/query.ts +++ b/frontend/src/queries/query.ts @@ -21,6 +21,7 @@ import { AnyPartialFilterType, OnlineExportContext, QueryExportContext } from '~ import { queryNodeToFilter } from './nodes/InsightQuery/utils/queryNodeToFilter' import { DataNode, HogQLQuery, HogQLQueryResponse, NodeKind, PersonsNode } from './schema' import { + isActorsQuery, isDataTableNode, isDataVisualizationNode, isEventsQuery, @@ -29,7 +30,6 @@ import { isInsightVizNode, isLifecycleQuery, isPersonsNode, - isPersonsQuery, isRetentionQuery, isTimeToSeeDataQuery, isTimeToSeeDataSessionsNode, @@ -52,7 +52,7 @@ export function queryExportContext( return queryExportContext(query.source, methodOptions, refresh) } else if (isDataVisualizationNode(query)) { return queryExportContext(query.source, methodOptions, refresh) - } else if (isEventsQuery(query) || isPersonsQuery(query)) { + } else if (isEventsQuery(query) || isActorsQuery(query)) { return { source: query, } diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 529fdc752f618..2e1a738ee29e5 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -68,6 +68,109 @@ "required": ["id", "kind"], "type": "object" }, + "ActorsQuery": { + "additionalProperties": false, + "properties": { + "fixedProperties": { + "items": { + "$ref": "#/definitions/AnyPropertyFilter" + }, + "type": "array" + }, + "kind": { + "const": "ActorsQuery", + "type": "string" + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "orderBy": { + "items": { + "type": "string" + }, + "type": "array" + }, + "properties": { + "items": { + "$ref": "#/definitions/AnyPropertyFilter" + }, + "type": "array" + }, + "response": { + "$ref": "#/definitions/ActorsQueryResponse", + "description": "Cached query response" + }, + "search": { + "type": "string" + }, + "select": { + "items": { + "$ref": "#/definitions/HogQLExpression" + }, + "type": "array" + }, + "source": { + "anyOf": [ + { + "$ref": "#/definitions/InsightActorsQuery" + }, + { + "$ref": "#/definitions/HogQLQuery" + } + ] + } + }, + "required": ["kind"], + "type": "object" + }, + "ActorsQueryResponse": { + "additionalProperties": false, + "properties": { + "columns": { + "items": {}, + "type": "array" + }, + "hasMore": { + "type": "boolean" + }, + "hogql": { + "type": "string" + }, + "limit": { + "type": "integer" + }, + "missing_actors_count": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "results": { + "items": { + "items": {}, + "type": "array" + }, + "type": "array" + }, + "timings": { + "items": { + "$ref": "#/definitions/QueryTiming" + }, + "type": "array" + }, + "types": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": ["results", "columns", "types", "hogql", "limit", "offset"], + "type": "object" + }, "AggregationAxisFormat": { "enum": ["numeric", "duration", "duration_ms", "percentage", "percentage_scaled"], "type": "string" @@ -90,10 +193,10 @@ "$ref": "#/definitions/EventsQuery" }, { - "$ref": "#/definitions/PersonsQuery" + "$ref": "#/definitions/ActorsQuery" }, { - "$ref": "#/definitions/InsightPersonsQuery" + "$ref": "#/definitions/InsightActorsQuery" }, { "$ref": "#/definitions/SessionsTimelineQuery" @@ -497,7 +600,7 @@ "$ref": "#/definitions/PersonsNode" }, { - "$ref": "#/definitions/PersonsQuery" + "$ref": "#/definitions/ActorsQuery" }, { "$ref": "#/definitions/HogQLQuery" @@ -1073,7 +1176,7 @@ "type": "array" } }, - "required": ["columns", "types", "results", "hogql", "limit", "offset"], + "required": ["columns", "types", "results", "hogql"], "type": "object" }, "FeaturePropertyFilter": { @@ -1672,6 +1775,33 @@ }, "type": "object" }, + "InsightActorsQuery": { + "additionalProperties": false, + "properties": { + "day": { + "type": "string" + }, + "interval": { + "description": "An interval selected out of available intervals in source query", + "type": "integer" + }, + "kind": { + "const": "InsightActorsQuery", + "type": "string" + }, + "response": { + "$ref": "#/definitions/ActorsQueryResponse" + }, + "source": { + "$ref": "#/definitions/InsightQueryNode" + }, + "status": { + "type": "string" + } + }, + "required": ["kind", "source"], + "type": "object" + }, "InsightFilter": { "anyOf": [ { @@ -1716,33 +1846,6 @@ ], "type": "string" }, - "InsightPersonsQuery": { - "additionalProperties": false, - "properties": { - "day": { - "type": "string" - }, - "interval": { - "description": "An interval selected out of available intervals in source query", - "type": "integer" - }, - "kind": { - "const": "InsightPersonsQuery", - "type": "string" - }, - "response": { - "$ref": "#/definitions/PersonsQueryResponse" - }, - "source": { - "$ref": "#/definitions/InsightQueryNode" - }, - "status": { - "type": "string" - } - }, - "required": ["kind", "source"], - "type": "object" - }, "InsightQueryNode": { "anyOf": [ { @@ -2011,7 +2114,7 @@ "PersonsNode", "HogQLQuery", "HogQLMetadata", - "PersonsQuery", + "ActorsQuery", "SessionsTimelineQuery", "DataTableNode", "DataVisualizationNode", @@ -2023,7 +2126,7 @@ "PathsQuery", "StickinessQuery", "LifecycleQuery", - "InsightPersonsQuery", + "InsightActorsQuery", "WebOverviewQuery", "WebTopClicksQuery", "WebStatsTableQuery", @@ -2229,109 +2332,6 @@ "required": ["kind"], "type": "object" }, - "PersonsQuery": { - "additionalProperties": false, - "properties": { - "fixedProperties": { - "items": { - "$ref": "#/definitions/AnyPropertyFilter" - }, - "type": "array" - }, - "kind": { - "const": "PersonsQuery", - "type": "string" - }, - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "orderBy": { - "items": { - "type": "string" - }, - "type": "array" - }, - "properties": { - "items": { - "$ref": "#/definitions/AnyPropertyFilter" - }, - "type": "array" - }, - "response": { - "$ref": "#/definitions/PersonsQueryResponse", - "description": "Cached query response" - }, - "search": { - "type": "string" - }, - "select": { - "items": { - "$ref": "#/definitions/HogQLExpression" - }, - "type": "array" - }, - "source": { - "anyOf": [ - { - "$ref": "#/definitions/InsightPersonsQuery" - }, - { - "$ref": "#/definitions/HogQLQuery" - } - ] - } - }, - "required": ["kind"], - "type": "object" - }, - "PersonsQueryResponse": { - "additionalProperties": false, - "properties": { - "columns": { - "items": {}, - "type": "array" - }, - "hasMore": { - "type": "boolean" - }, - "hogql": { - "type": "string" - }, - "limit": { - "type": "integer" - }, - "missing_actors_count": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "results": { - "items": { - "items": {}, - "type": "array" - }, - "type": "array" - }, - "timings": { - "items": { - "$ref": "#/definitions/QueryTiming" - }, - "type": "array" - }, - "types": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": ["results", "columns", "types", "hogql", "limit", "offset"], - "type": "object" - }, "PropertyFilterType": { "enum": [ "meta", diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 94b739a189871..a5787319fe85e 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -46,7 +46,7 @@ export enum NodeKind { PersonsNode = 'PersonsNode', HogQLQuery = 'HogQLQuery', HogQLMetadata = 'HogQLMetadata', - PersonsQuery = 'PersonsQuery', + ActorsQuery = 'ActorsQuery', SessionsTimelineQuery = 'SessionsTimelineQuery', // Interface nodes @@ -62,7 +62,7 @@ export enum NodeKind { PathsQuery = 'PathsQuery', StickinessQuery = 'StickinessQuery', LifecycleQuery = 'LifecycleQuery', - InsightPersonsQuery = 'InsightPersonsQuery', + InsightActorsQuery = 'InsightActorsQuery', // Web analytics queries WebOverviewQuery = 'WebOverviewQuery', @@ -85,8 +85,8 @@ export type AnyDataNode = | PersonsNode // old persons API endpoint | TimeToSeeDataSessionsQuery // old API | EventsQuery - | PersonsQuery - | InsightPersonsQuery + | ActorsQuery + | InsightActorsQuery | SessionsTimelineQuery | HogQLQuery | HogQLMetadata @@ -260,9 +260,9 @@ export interface EventsQueryResponse { hasMore?: boolean timings?: QueryTiming[] /** @asType integer */ - limit: number + limit?: number /** @asType integer */ - offset: number + offset?: number } export interface EventsQueryPersonColumn { uuid: string @@ -336,7 +336,7 @@ export interface DataTableNode extends Node, DataTableNodeViewProps { | EventsNode | EventsQuery | PersonsNode - | PersonsQuery + | ActorsQuery | HogQLQuery | TimeToSeeDataSessionsQuery | WebOverviewQuery @@ -631,7 +631,7 @@ export interface LifecycleQuery extends Omit { diff --git a/frontend/src/queries/utils.ts b/frontend/src/queries/utils.ts index 11dd7a60db071..c6c12e8a3a03f 100644 --- a/frontend/src/queries/utils.ts +++ b/frontend/src/queries/utils.ts @@ -4,6 +4,7 @@ import { teamLogic } from 'scenes/teamLogic' import { ActionsNode, + ActorsQuery, DatabaseSchemaQuery, DataTableNode, DataVisualizationNode, @@ -13,10 +14,10 @@ import { FunnelsQuery, HogQLMetadata, HogQLQuery, + InsightActorsQuery, InsightFilter, InsightFilterProperty, InsightNodeKind, - InsightPersonsQuery, InsightQueryNode, InsightVizNode, LifecycleQuery, @@ -24,7 +25,6 @@ import { NodeKind, PathsQuery, PersonsNode, - PersonsQuery, RetentionQuery, SavedInsightNode, StickinessQuery, @@ -46,7 +46,7 @@ export function isDataNode(node?: Node | null): node is EventsQuery | PersonsNod isPersonsNode(node) || isTimeToSeeDataSessionsQuery(node) || isEventsQuery(node) || - isPersonsQuery(node) || + isActorsQuery(node) || isHogQLQuery(node) || isHogQLMetadata(node) ) @@ -92,12 +92,12 @@ export function isPersonsNode(node?: Node | null): node is PersonsNode { return node?.kind === NodeKind.PersonsNode } -export function isPersonsQuery(node?: Node | null): node is PersonsQuery { - return node?.kind === NodeKind.PersonsQuery +export function isActorsQuery(node?: Node | null): node is ActorsQuery { + return node?.kind === NodeKind.ActorsQuery } -export function isInsightPersonsQuery(node?: Node | null): node is InsightPersonsQuery { - return node?.kind === NodeKind.InsightPersonsQuery +export function isInsightActorsQuery(node?: Node | null): node is InsightActorsQuery { + return node?.kind === NodeKind.InsightActorsQuery } export function isDataTableNode(node?: Node | null): node is DataTableNode { @@ -183,10 +183,10 @@ export function isDatabaseSchemaQuery(node?: Node): node is DatabaseSchemaQuery return node?.kind === NodeKind.DatabaseSchemaQuery } -export function isQueryForGroup(query: PersonsNode | PersonsQuery): boolean { +export function isQueryForGroup(query: PersonsNode | ActorsQuery): boolean { return ( - isPersonsQuery(query) && - isInsightPersonsQuery(query.source) && + isActorsQuery(query) && + isInsightActorsQuery(query.source) && isRetentionQuery(query.source.source) && query.source.source.aggregation_group_type_index !== undefined ) diff --git a/frontend/src/scenes/annotations/Annotations.tsx b/frontend/src/scenes/annotations/Annotations.tsx index 90762f8c3bc81..110d8794fd0b1 100644 --- a/frontend/src/scenes/annotations/Annotations.tsx +++ b/frontend/src/scenes/annotations/Annotations.tsx @@ -102,8 +102,7 @@ export function Annotations(): JSX.Element { return (
- - - +
+ - +
+

Delivery trends

- +
- +
+

Errors

- +
) } diff --git a/frontend/src/scenes/authentication/InviteSignup.tsx b/frontend/src/scenes/authentication/InviteSignup.tsx index 53fdf23307631..ae76c03a89c4d 100644 --- a/frontend/src/scenes/authentication/InviteSignup.tsx +++ b/frontend/src/scenes/authentication/InviteSignup.tsx @@ -143,7 +143,7 @@ function AuthenticatedAcceptInvite({ invite }: { invite: PrevalidatedInvite }): className="border rounded-lg border-dashed flex items-center gap-2 px-2 py-1" data-attr="top-navigation-whoami" > - +
{user.first_name}
{user.organization?.name}
diff --git a/frontend/src/scenes/cohorts/cohortEditLogic.ts b/frontend/src/scenes/cohorts/cohortEditLogic.ts index ecfba636b35c8..8cd8b06f94dba 100644 --- a/frontend/src/scenes/cohorts/cohortEditLogic.ts +++ b/frontend/src/scenes/cohorts/cohortEditLogic.ts @@ -70,7 +70,7 @@ export const cohortEditLogic = kea([ }), selectors({ - usePersonsQuery: [(s) => [s.featureFlags], (featureFlags) => featureFlags[FEATURE_FLAGS.PERSONS_HOGQL_QUERY]], + useActorsQuery: [(s) => [s.featureFlags], (featureFlags) => featureFlags[FEATURE_FLAGS.PERSONS_HOGQL_QUERY]], }), reducers(({ props, selectors }) => ({ @@ -167,11 +167,11 @@ export const cohortEditLogic = kea([ ], query: [ ((state: Record) => - selectors.usePersonsQuery(state) + selectors.useActorsQuery(state) ? { kind: NodeKind.DataTableNode, source: { - kind: NodeKind.PersonsQuery, + kind: NodeKind.ActorsQuery, fixedProperties: [ { type: PropertyFilterType.Cohort, key: 'id', value: parseInt(String(props.id)) }, ], diff --git a/frontend/src/scenes/dashboard/DashboardCollaborators.tsx b/frontend/src/scenes/dashboard/DashboardCollaborators.tsx index 3f43a36e43d42..bb61ce0f55110 100644 --- a/frontend/src/scenes/dashboard/DashboardCollaborators.tsx +++ b/frontend/src/scenes/dashboard/DashboardCollaborators.tsx @@ -121,7 +121,7 @@ function CollaboratorRow({ return (
- + )} {featureFlags[FEATURE_FLAGS.ROLE_BASED_ACCESS] && ( - - - setRolesToAdd(roleIds)} - rolesToAdd={rolesToAdd} - addableRoles={addableRoles} - addableRolesLoading={unfilteredAddableRolesLoading} - onAdd={() => addAssociatedRoles()} - roles={derivedRoles} - deleteAssociatedRole={(id) => - deleteAssociatedRole({ roleId: id }) - } - canEdit={featureFlag.can_edit} - /> - - +
+

Permissions

+ +
+ + setRolesToAdd(roleIds)} + rolesToAdd={rolesToAdd} + addableRoles={addableRoles} + addableRolesLoading={unfilteredAddableRolesLoading} + onAdd={() => addAssociatedRoles()} + roles={derivedRoles} + deleteAssociatedRole={(id) => + deleteAssociatedRole({ roleId: id }) + } + canEdit={featureFlag.can_edit} + /> + +
+
)} )} diff --git a/frontend/src/scenes/feature-flags/FeatureFlagCodeOptions.tsx b/frontend/src/scenes/feature-flags/FeatureFlagCodeOptions.tsx index 9ef72f4e779b5..2d5d2c618d6ae 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagCodeOptions.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagCodeOptions.tsx @@ -118,7 +118,16 @@ export const OPTIONS: InstructionOption[] = [ export const LOCAL_EVALUATION_LIBRARIES: string[] = [SDKKey.NODE_JS, SDKKey.PYTHON, SDKKey.RUBY, SDKKey.PHP, SDKKey.GO] -export const PAYLOAD_LIBRARIES: string[] = [SDKKey.JS_WEB, SDKKey.NODE_JS, SDKKey.PYTHON, SDKKey.RUBY, SDKKey.REACT] +export const PAYLOAD_LIBRARIES: string[] = [ + SDKKey.JS_WEB, + SDKKey.NODE_JS, + SDKKey.PYTHON, + SDKKey.RUBY, + SDKKey.REACT, + SDKKey.ANDROID, + SDKKey.REACT_NATIVE, + SDKKey.IOS, +] export const BOOTSTRAPPING_OPTIONS: InstructionOption[] = [ { diff --git a/frontend/src/scenes/feature-flags/FeatureFlagSnippets.tsx b/frontend/src/scenes/feature-flags/FeatureFlagSnippets.tsx index af476aa9b903d..5e50403e26d58 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagSnippets.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagSnippets.tsx @@ -312,8 +312,17 @@ if ${conditional}: ) } -export function AndroidSnippet({ flagKey, multivariant }: FeatureFlagSnippet): JSX.Element { +export function AndroidSnippet({ flagKey, multivariant, payload }: FeatureFlagSnippet): JSX.Element { const clientSuffix = 'PostHog.' + + if (payload) { + return ( + + {`${clientSuffix}getFeatureFlagPayload("${flagKey}")`} + + ) + } + const flagFunction = multivariant ? 'getFeatureFlag' : 'isFeatureEnabled' const variantSuffix = multivariant ? ` == "example-variant"` : '' @@ -327,8 +336,17 @@ export function AndroidSnippet({ flagKey, multivariant }: FeatureFlagSnippet): J ) } -export function iOSSnippet({ flagKey, multivariant }: FeatureFlagSnippet): JSX.Element { +export function iOSSnippet({ flagKey, multivariant, payload }: FeatureFlagSnippet): JSX.Element { const clientSuffix = 'posthog.' + + if (payload) { + return ( + + {`${clientSuffix}getFeatureFlagStringPayload("${flagKey}", defaultValue: "myDefaultValue")`} + + ) + } + const flagFunction = multivariant ? 'getFeatureFlag' : 'isFeatureEnabled' const variantSuffix = multivariant ? ` == 'example-variant'` : '' @@ -344,8 +362,17 @@ if (${clientSuffix}${flagFunction}('${flagKey}') ${variantSuffix}) { ) } -export function ReactNativeSnippet({ flagKey, multivariant }: FeatureFlagSnippet): JSX.Element { +export function ReactNativeSnippet({ flagKey, multivariant, payload }: FeatureFlagSnippet): JSX.Element { const clientSuffix = 'posthog.' + + if (payload) { + return ( + + {`${clientSuffix}getFeatureFlagPayload('${flagKey}'`} + + ) + } + const flagFunction = multivariant ? 'getFeatureFlag' : 'isFeatureEnabled' const variantSuffix = multivariant ? ` == 'example-variant'` : '' diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelation.scss b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelation.scss index d1932ec322b9d..e960fc44d021f 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelation.scss +++ b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelation.scss @@ -4,33 +4,18 @@ .skew-warning { margin-top: 1rem; line-height: 2em; + background-color: var(--bg-light); border: 1px solid var(--warning); - - .ant-card-body { - padding: 0.5rem 1rem; - } + border-radius: var(--radius); h4 { position: relative; display: flex; align-items: center; - padding: 0.5rem 1rem; - margin-right: -1rem; - margin-left: -1rem; + justify-content: space-between; + padding: 0.5rem; font-size: 1.1em; border-bottom: 1px solid var(--border); - - .close-button { - position: absolute; - right: 16px; - color: var(--muted) !important; - cursor: pointer; - } - } - - b { - padding-bottom: 0.5rem; - font-weight: var(--font-medium); } } } diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationSkewWarning.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationSkewWarning.tsx index 6e4cc1416ffe0..ef40e6a9e3a57 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationSkewWarning.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationSkewWarning.tsx @@ -1,8 +1,6 @@ -// eslint-disable-next-line no-restricted-imports -import { CloseOutlined } from '@ant-design/icons' -import { Card } from 'antd' +import { LemonButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { IconFeedback } from 'lib/lemon-ui/icons' +import { IconClose, IconFeedback } from 'lib/lemon-ui/icons' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' import { insightLogic } from 'scenes/insights/insightLogic' @@ -16,14 +14,16 @@ export const FunnelCorrelationSkewWarning = (): JSX.Element | null => { } return ( - +

- Adjust your funnel - definition to improve correlation analysis - +
+ + Adjust your funnel definition to improve correlation analysis +
+ } onClick={hideSkewWarning} />

-
- Tips for adjusting your funnel: +
+ Tips for adjusting your funnel:
  1. Adjust your first funnel step to be more specific. For example, choose a page or an event that @@ -32,6 +32,6 @@ export const FunnelCorrelationSkewWarning = (): JSX.Element | null => {
  2. Choose an event that happens more frequently for subsequent funnels steps.
- +
) } diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelPropertyCorrelationTable.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelPropertyCorrelationTable.tsx index fbede9951a858..01bb851c8f3b6 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelPropertyCorrelationTable.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelPropertyCorrelationTable.tsx @@ -2,6 +2,7 @@ import './FunnelCorrelationTable.scss' import { IconInfo } from '@posthog/icons' import { LemonButton, LemonCheckbox } from '@posthog/lemon-ui' +// eslint-disable-next-line no-restricted-imports import { Col, ConfigProvider, Empty, Row, Table } from 'antd' import Column from 'antd/lib/table/Column' import { useActions, useValues } from 'kea' diff --git a/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrations.tsx b/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrations.tsx index 4bc890c80f38a..30ca2815d47f0 100644 --- a/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrations.tsx +++ b/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrations.tsx @@ -1,5 +1,5 @@ import { Link } from '@posthog/lemon-ui' -import { Button, Progress } from 'antd' +import { Progress } from 'antd' import { useActions, useValues } from 'kea' import { PageHeader } from 'lib/components/PageHeader' import { IconPlayCircle, IconRefresh, IconReplay } from 'lib/lemon-ui/icons' @@ -151,13 +151,13 @@ export function AsyncMigrations(): JSX.Element {
{status === AsyncMigrationStatus.NotStarted || status === AsyncMigrationStatus.FailedAtStartup ? ( - + ) : status === AsyncMigrationStatus.Starting || status === AsyncMigrationStatus.Running ? ( + return }, width: 32, }, diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeCohort.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeCohort.tsx index fd5aad3420b5e..310ec587c8d14 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeCohort.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeCohort.tsx @@ -53,7 +53,7 @@ const Component = ({ attributes }: NotebookNodeProps { - +
{member?.user.first_name}
{member?.user.email}
diff --git a/frontend/src/scenes/notebooks/Notebook/MentionsExtension.tsx b/frontend/src/scenes/notebooks/Notebook/MentionsExtension.tsx index d5c98de3f94ac..eafec59d4c0cb 100644 --- a/frontend/src/scenes/notebooks/Notebook/MentionsExtension.tsx +++ b/frontend/src/scenes/notebooks/Notebook/MentionsExtension.tsx @@ -121,7 +121,7 @@ export const Mentions = forwardRef(function SlashCom key={member.id} fullWidth status="primary-alt" - icon={} + icon={} active={index === selectedIndex} onClick={() => void execute(member)} > diff --git a/frontend/src/scenes/notebooks/Notebook/NotebookHistory.tsx b/frontend/src/scenes/notebooks/Notebook/NotebookHistory.tsx index 702182c5a05fe..3f72641d328e0 100644 --- a/frontend/src/scenes/notebooks/Notebook/NotebookHistory.tsx +++ b/frontend/src/scenes/notebooks/Notebook/NotebookHistory.tsx @@ -51,9 +51,11 @@ function NotebookHistoryList({ onItemClick }: { onItemClick: (logItem: ActivityL const buttonContent = ( diff --git a/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx b/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx index 7dcf4b57e3088..48be40ae066bb 100644 --- a/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx +++ b/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx @@ -57,8 +57,7 @@ function NotebooksChoiceList(props: { sideIcon={ notebook.created_by ? ( `} /> diff --git a/frontend/src/scenes/persons-management/tabs/personsSceneLogic.ts b/frontend/src/scenes/persons-management/tabs/personsSceneLogic.ts index a96c0291d2379..4ab64ba0815f6 100644 --- a/frontend/src/scenes/persons-management/tabs/personsSceneLogic.ts +++ b/frontend/src/scenes/persons-management/tabs/personsSceneLogic.ts @@ -7,10 +7,10 @@ import { DataTableNode, Node, NodeKind } from '~/queries/schema' import type { personsSceneLogicType } from './personsSceneLogicType' -const getDefaultQuery = (usePersonsQuery = false): DataTableNode => ({ +const getDefaultQuery = (useActorsQuery = false): DataTableNode => ({ kind: NodeKind.DataTableNode, - source: usePersonsQuery - ? { kind: NodeKind.PersonsQuery, select: defaultDataTableColumns(NodeKind.PersonsQuery) } + source: useActorsQuery + ? { kind: NodeKind.ActorsQuery, select: defaultDataTableColumns(NodeKind.ActorsQuery) } : { kind: NodeKind.PersonsNode }, full: true, propertiesViaUrl: true, diff --git a/frontend/src/scenes/persons/PersonDisplay.tsx b/frontend/src/scenes/persons/PersonDisplay.tsx index 98b349859b245..f8dea0bd46fdf 100644 --- a/frontend/src/scenes/persons/PersonDisplay.tsx +++ b/frontend/src/scenes/persons/PersonDisplay.tsx @@ -30,7 +30,7 @@ export interface PersonDisplayProps { export function PersonIcon({ person, ...props -}: Pick & Omit): JSX.Element { +}: Pick & Omit): JSX.Element { const display = asDisplay(person) const email: string | undefined = useMemo(() => { @@ -41,7 +41,15 @@ export function PersonIcon({ return typeof possibleEmail === 'string' ? possibleEmail : undefined }, [person?.properties?.email]) - return + return ( + + ) } export function PersonDisplay({ diff --git a/frontend/src/scenes/plugins/edit/PluginField.tsx b/frontend/src/scenes/plugins/edit/PluginField.tsx index 48773fc765898..bfd03a84b1221 100644 --- a/frontend/src/scenes/plugins/edit/PluginField.tsx +++ b/frontend/src/scenes/plugins/edit/PluginField.tsx @@ -1,5 +1,5 @@ +import { LemonButton, LemonInput, LemonSelect } from '@posthog/lemon-ui' import { PluginConfigSchema } from '@posthog/plugin-scaffold/src/types' -import { Button, Input, Select } from 'antd' import { CodeEditor } from 'lib/components/CodeEditors' import { IconEdit } from 'lib/lemon-ui/icons' import { useState } from 'react' @@ -50,7 +50,8 @@ export function PluginField({ (value === SECRET_FIELD_VALUE || value.name === SECRET_FIELD_VALUE) ) { return ( - + ) } return fieldConfig.type === 'attachment' ? ( ) : fieldConfig.type === 'string' ? ( - + ) : fieldConfig.type === 'json' ? ( ) : fieldConfig.type === 'choice' ? ( - + { + return { label: choice, value: choice } + })} + /> ) : ( Unknown field type "{fieldConfig.type}". diff --git a/frontend/src/scenes/plugins/source/PluginSource.tsx b/frontend/src/scenes/plugins/source/PluginSource.tsx index ad47b039462bf..11c0dccb39155 100644 --- a/frontend/src/scenes/plugins/source/PluginSource.tsx +++ b/frontend/src/scenes/plugins/source/PluginSource.tsx @@ -2,7 +2,7 @@ import './PluginSource.scss' import { useMonaco } from '@monaco-editor/react' import { Link } from '@posthog/lemon-ui' -import { Button, Skeleton } from 'antd' +import { Skeleton } from 'antd' import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' import { CodeEditor } from 'lib/components/CodeEditors' @@ -81,13 +81,11 @@ export function PluginSource({ title={pluginSourceLoading ? 'Loading...' : `Edit App: ${name}`} placement={placement ?? 'left'} footer={ -
- - +
} > diff --git a/frontend/src/scenes/retention/queries.ts b/frontend/src/scenes/retention/queries.ts index 3bca06cca714e..7b8d738cc7109 100644 --- a/frontend/src/scenes/retention/queries.ts +++ b/frontend/src/scenes/retention/queries.ts @@ -1,17 +1,17 @@ import { RetentionTableAppearanceType, RetentionTablePeoplePayload } from 'scenes/retention/types' import { query } from '~/queries/query' -import { NodeKind, PersonsQuery, RetentionQuery } from '~/queries/schema' +import { ActorsQuery, NodeKind, RetentionQuery } from '~/queries/schema' -export function retentionToActorsQuery(query: RetentionQuery, selectedInterval: number, offset = 0): PersonsQuery { +export function retentionToActorsQuery(query: RetentionQuery, selectedInterval: number, offset = 0): ActorsQuery { const group = query.aggregation_group_type_index !== undefined const select = group ? 'group' : 'person' return { - kind: NodeKind.PersonsQuery, + kind: NodeKind.ActorsQuery, select: [select, 'appearances'], orderBy: ['length(appearances) DESC', 'actor_id'], source: { - kind: NodeKind.InsightPersonsQuery, + kind: NodeKind.InsightActorsQuery, interval: selectedInterval, source: { ...query, diff --git a/frontend/src/scenes/retention/retentionModalLogic.ts b/frontend/src/scenes/retention/retentionModalLogic.ts index 4ad79ea1407c4..ad605e13b1516 100644 --- a/frontend/src/scenes/retention/retentionModalLogic.ts +++ b/frontend/src/scenes/retention/retentionModalLogic.ts @@ -5,8 +5,8 @@ import { retentionToActorsQuery } from 'scenes/retention/queries' import { urls } from 'scenes/urls' import { groupsModel, Noun } from '~/models/groupsModel' -import { DataTableNode, NodeKind, PersonsQuery, RetentionQuery } from '~/queries/schema' -import { isInsightPersonsQuery, isLifecycleQuery, isRetentionQuery, isStickinessQuery } from '~/queries/utils' +import { ActorsQuery, DataTableNode, NodeKind, RetentionQuery } from '~/queries/schema' +import { isInsightActorsQuery, isLifecycleQuery, isRetentionQuery, isStickinessQuery } from '~/queries/utils' import { InsightLogicProps } from '~/types' import type { retentionModalLogicType } from './retentionModalLogicType' @@ -46,9 +46,9 @@ export const retentionModalLogic = kea([ return aggregationLabel(aggregation_group_type_index) }, ], - personsQuery: [ + actorsQuery: [ (s) => [s.querySource, s.selectedInterval], - (querySource: RetentionQuery, selectedInterval): PersonsQuery | null => { + (querySource: RetentionQuery, selectedInterval): ActorsQuery | null => { if (!querySource) { return null } @@ -56,20 +56,20 @@ export const retentionModalLogic = kea([ }, ], exploreUrl: [ - (s) => [s.personsQuery], - (personsQuery): string | null => { - if (!personsQuery) { + (s) => [s.actorsQuery], + (actorsQuery): string | null => { + if (!actorsQuery) { return null } const query: DataTableNode = { kind: NodeKind.DataTableNode, - source: personsQuery, + source: actorsQuery, full: true, } if ( - isInsightPersonsQuery(personsQuery.source) && - isRetentionQuery(personsQuery.source.source) && - personsQuery.source.source.aggregation_group_type_index !== undefined + isInsightActorsQuery(actorsQuery.source) && + isRetentionQuery(actorsQuery.source.source) && + actorsQuery.source.source.aggregation_group_type_index !== undefined ) { query.showPropertyFilter = false } diff --git a/frontend/src/scenes/saved-insights/SavedInsights.tsx b/frontend/src/scenes/saved-insights/SavedInsights.tsx index 7706c2932886a..38a34431b8b7b 100644 --- a/frontend/src/scenes/saved-insights/SavedInsights.tsx +++ b/frontend/src/scenes/saved-insights/SavedInsights.tsx @@ -185,13 +185,13 @@ export const QUERY_TYPES_METADATA: Record = { icon: IconPerson, inMenu: true, }, - [NodeKind.PersonsQuery]: { + [NodeKind.ActorsQuery]: { name: 'Persons', description: 'List of persons matching specified conditions', icon: IconPerson, inMenu: false, }, - [NodeKind.InsightPersonsQuery]: { + [NodeKind.InsightActorsQuery]: { name: 'Persons', description: 'List of persons matching specified conditions, derived from an insight', icon: IconPerson, diff --git a/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.ts b/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.ts index e5a07345c1d01..1cf1fc3f23c68 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.ts +++ b/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.ts @@ -81,6 +81,7 @@ const PostHogMobileEvents = [ 'Application Backgrounded', 'Application Updated', 'Application Installed', + 'Application Became Active', ] function isPostHogEvent(item: InspectorListItemEvent): boolean { diff --git a/frontend/src/scenes/settings/organization/Invites.tsx b/frontend/src/scenes/settings/organization/Invites.tsx index 132184c745226..823f24997fb14 100644 --- a/frontend/src/scenes/settings/organization/Invites.tsx +++ b/frontend/src/scenes/settings/organization/Invites.tsx @@ -62,7 +62,7 @@ export function Invites(): JSX.Element { { key: 'user_profile_picture', render: function ProfilePictureRender(_, invite) { - return + return }, width: 32, }, diff --git a/frontend/src/scenes/settings/organization/Members.tsx b/frontend/src/scenes/settings/organization/Members.tsx index c892bbd7dc4e9..ae4a31509a537 100644 --- a/frontend/src/scenes/settings/organization/Members.tsx +++ b/frontend/src/scenes/settings/organization/Members.tsx @@ -10,6 +10,7 @@ import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { fullName } from 'lib/utils' import { getReasonForAccessLevelChangeProhibition, membershipLevelToName, @@ -68,7 +69,7 @@ function ActionsComponent(_: any, member: OrganizationMemberType): JSX.Element | } if (listLevel === OrganizationMembershipLevel.Owner) { LemonDialog.open({ - title: `Transfer organization ownership to ${member.user.first_name}?`, + title: `Transfer organization ownership to ${fullName(member.user)}?`, description: `You will no longer be the owner of ${user.organization?.name}. After the transfer you will become an administrator.`, primaryButton: { status: 'danger', @@ -109,7 +110,7 @@ function ActionsComponent(_: any, member: OrganizationMemberType): JSX.Element | title: `${ member.user.uuid == user.uuid ? 'Leave' - : `Remove ${member.user.first_name} from` + : `Remove ${fullName(member.user)} from` } organization ${user.organization?.name}?`, primaryButton: { children: member.user.uuid == user.uuid ? 'Leave' : 'Remove', @@ -150,16 +151,16 @@ export function Members(): JSX.Element | null { { key: 'user_profile_picture', render: function ProfilePictureRender(_, member) { - return + return }, width: 32, }, { title: 'Name', - key: 'user_first_name', + key: 'user_name', render: (_, member) => - member.user.uuid == user.uuid ? `${member.user.first_name} (me)` : member.user.first_name, - sorter: (a, b) => a.user.first_name.localeCompare(b.user.first_name), + member.user.uuid == user.uuid ? `${fullName(member.user)} (me)` : fullName(member.user), + sorter: (a, b) => fullName(a.user).localeCompare(fullName(b.user)), }, { title: 'Email', diff --git a/frontend/src/scenes/settings/organization/Permissions/Roles/CreateRoleModal.tsx b/frontend/src/scenes/settings/organization/Permissions/Roles/CreateRoleModal.tsx index 3311cc1833117..fc2d6571dbf2d 100644 --- a/frontend/src/scenes/settings/organization/Permissions/Roles/CreateRoleModal.tsx +++ b/frontend/src/scenes/settings/organization/Permissions/Roles/CreateRoleModal.tsx @@ -154,7 +154,7 @@ function MemberRow({ return (
- + {isAdminOrOwner && deleteMember && ( } diff --git a/frontend/src/scenes/settings/project/ProjectAccessControl.tsx b/frontend/src/scenes/settings/project/ProjectAccessControl.tsx index b34d3a52fd94c..ff18dd636e143 100644 --- a/frontend/src/scenes/settings/project/ProjectAccessControl.tsx +++ b/frontend/src/scenes/settings/project/ProjectAccessControl.tsx @@ -140,7 +140,7 @@ export function ProjectTeamMembers(): JSX.Element | null { { key: 'user_profile_picture', render: function ProfilePictureRender(_, member) { - return + return }, width: 32, }, diff --git a/frontend/src/scenes/surveys/SurveyAppearance.tsx b/frontend/src/scenes/surveys/SurveyAppearance.tsx index ef0d568f3ceb4..dbf5349ad3f8f 100644 --- a/frontend/src/scenes/surveys/SurveyAppearance.tsx +++ b/frontend/src/scenes/surveys/SurveyAppearance.tsx @@ -56,7 +56,7 @@ interface ButtonProps { children: React.ReactNode } -const Button = ({ +const SurveyButton = ({ link, type, onSubmit, @@ -351,7 +351,7 @@ export function BaseAppearance({
- +
{!preview && !appearance.whiteLabel && ( @@ -572,13 +572,13 @@ export function SurveyRatingAppearance({
- +
{!preview && !appearance.whiteLabel && ( @@ -739,9 +739,13 @@ export function SurveyMultipleChoiceAppearance({
- +
{!preview && !appearance.whiteLabel && ( @@ -797,9 +801,9 @@ export function SurveyThankYou({ appearance }: { appearance: SurveyAppearanceTyp className="thank-you-message-body" dangerouslySetInnerHTML={{ __html: sanitizeHTML(appearance?.thankYouMessageDescription || '') }} /> - + {!appearance.whiteLabel && ( Survey by {posthogLogoSVG} diff --git a/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx b/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx index bce7f2176b9db..d07dde60bf662 100644 --- a/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx +++ b/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx @@ -25,7 +25,7 @@ import { sessionPlayerModalLogic } from 'scenes/session-recordings/player/modal/ import { teamLogic } from 'scenes/teamLogic' import { Noun } from '~/models/groupsModel' -import { InsightPersonsQuery } from '~/queries/schema' +import { InsightActorsQuery } from '~/queries/schema' import { ActorType, ExporterFormat, @@ -39,7 +39,7 @@ import { SaveCohortModal } from './SaveCohortModal' export interface PersonsModalProps extends Pick { onAfterClose?: () => void - query?: InsightPersonsQuery | null + query?: InsightActorsQuery | null url?: string | null urlsIndex?: number urls?: { @@ -77,7 +77,7 @@ export function PersonsModal({ missingActorsCount, propertiesTimelineFilterFromUrl, exploreUrl, - personsQuery, + ActorsQuery, } = useValues(logic) const { setSearchTerm, saveAsCohort, setIsCohortModalOpen, closeModal, loadNextActors } = useActions(logic) const { openSessionPlayer } = useActions(sessionPlayerModalLogic) @@ -191,7 +191,7 @@ export function PersonsModal({ void triggerExport({ export_format: ExporterFormat.CSV, export_context: query - ? { source: personsQuery as Record } + ? { source: ActorsQuery as Record } : { path: originalUrl }, }) }} diff --git a/frontend/src/scenes/trends/persons-modal/personsModalLogic.ts b/frontend/src/scenes/trends/persons-modal/personsModalLogic.ts index b8a54b03094d2..da64e7aadd17d 100644 --- a/frontend/src/scenes/trends/persons-modal/personsModalLogic.ts +++ b/frontend/src/scenes/trends/persons-modal/personsModalLogic.ts @@ -11,7 +11,7 @@ import { urls } from 'scenes/urls' import { cohortsModel } from '~/models/cohortsModel' import { groupsModel } from '~/models/groupsModel' import { query as performQuery } from '~/queries/query' -import { DataTableNode, InsightPersonsQuery, NodeKind, PersonsQuery } from '~/queries/schema' +import { ActorsQuery, DataTableNode, InsightActorsQuery, NodeKind } from '~/queries/schema' import { ActorType, BreakdownType, @@ -26,7 +26,7 @@ import type { personsModalLogicType } from './personsModalLogicType' const RESULTS_PER_PAGE = 100 export interface PersonModalLogicProps { - query?: InsightPersonsQuery | null + query?: InsightActorsQuery | null url?: string | null } @@ -56,7 +56,7 @@ export const personsModalLogic = kea([ offset, }: { url?: string | null - query?: InsightPersonsQuery | null + query?: InsightActorsQuery | null clear?: boolean offset?: number }) => ({ @@ -92,10 +92,10 @@ export const personsModalLogic = kea([ return res } else if (query) { const response = await performQuery({ - ...values.personsQuery, + ...values.ActorsQuery, limit: RESULTS_PER_PAGE + 1, offset: offset || 0, - } as PersonsQuery) + } as ActorsQuery) const newResponse: ListActorsResponse = { results: [ { @@ -180,8 +180,8 @@ export const personsModalLogic = kea([ is_static: true, name: cohortName, } - if (values.personsQuery) { - const cohort = await api.create('api/cohort', { ...cohortParams, query: values.personsQuery }) + if (values.ActorsQuery) { + const cohort = await api.create('api/cohort', { ...cohortParams, query: values.ActorsQuery }) cohortsModel.actions.cohortCreated(cohort) lemonToast.success('Cohort saved', { toastId: `cohort-saved-${cohort.id}`, @@ -256,14 +256,14 @@ export const personsModalLogic = kea([ return cleanFilters(filter) }, ], - personsQuery: [ + ActorsQuery: [ (s) => [(_, p) => p.query, s.searchTerm], - (query, searchTerm): PersonsQuery | null => { + (query, searchTerm): ActorsQuery | null => { if (!query) { return null } return { - kind: NodeKind.PersonsQuery, + kind: NodeKind.ActorsQuery, source: query, select: ['person', 'created_at'], orderBy: ['created_at DESC'], @@ -272,12 +272,12 @@ export const personsModalLogic = kea([ }, ], exploreUrl: [ - (s) => [s.personsQuery], - (personsQuery): string | null => { - if (!personsQuery) { + (s) => [s.ActorsQuery], + (ActorsQuery): string | null => { + if (!ActorsQuery) { return null } - const { select: _select, ...source } = personsQuery + const { select: _select, ...source } = ActorsQuery const query: DataTableNode = { kind: NodeKind.DataTableNode, source, diff --git a/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx b/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx index 42d31f40ea631..a80f2cdd279b1 100644 --- a/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx +++ b/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx @@ -119,7 +119,7 @@ export function ActionsLineGraph({ openPersonsModal({ title, query: { - kind: NodeKind.InsightPersonsQuery, + kind: NodeKind.InsightActorsQuery, source: query.source, day, status: dataset.status, diff --git a/frontend/src/stories/How to build a form.stories.mdx b/frontend/src/stories/How to build a form.stories.mdx index 37b69133ab6eb..504cd7714e08c 100644 --- a/frontend/src/stories/How to build a form.stories.mdx +++ b/frontend/src/stories/How to build a form.stories.mdx @@ -163,7 +163,7 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element { )} - + ) } diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 31fd16548269b..3053e08502407 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -147,6 +147,7 @@ interface UserBaseType { uuid: string distinct_id: string first_name: string + last_name?: string email: string } diff --git a/latest_migrations.manifest b/latest_migrations.manifest index 5a6f653aeef6e..9deccea94b408 100644 --- a/latest_migrations.manifest +++ b/latest_migrations.manifest @@ -5,7 +5,7 @@ contenttypes: 0002_remove_content_type_name ee: 0015_add_verified_properties otp_static: 0002_throttling otp_totp: 0002_auto_20190420_0723 -posthog: 0378_alter_user_theme_mode +posthog: 0379_alter_scheduledchange sessions: 0001_initial social_django: 0010_uid_db_index two_factor: 0007_auto_20201201_1019 diff --git a/posthog/api/cohort.py b/posthog/api/cohort.py index 68f2809f6a484..713bb733de220 100644 --- a/posthog/api/cohort.py +++ b/posthog/api/cohort.py @@ -79,7 +79,7 @@ from posthog.queries.trends.trends_actors import TrendsActors from posthog.queries.trends.lifecycle_actors import LifecycleActors from posthog.queries.util import get_earliest_timestamp -from posthog.schema import PersonsQuery +from posthog.schema import ActorsQuery from posthog.tasks.calculate_cohort import ( calculate_cohort_from_list, insert_cohort_from_feature_flag, @@ -180,9 +180,9 @@ def validate_query(self, query: Optional[Dict]) -> Optional[Dict]: return None if not isinstance(query, dict): raise ValidationError("Query must be a dictionary.") - if query.get("kind") != "PersonsQuery": - raise ValidationError(f"Query must be a PersonsQuery. Got: {query.get('kind')}") - PersonsQuery.model_validate(query) + if query.get("kind") != "ActorsQuery": + raise ValidationError(f"Query must be a ActorsQuery. Got: {query.get('kind')}") + ActorsQuery.model_validate(query) return query def validate_filters(self, request_filters: Dict): diff --git a/posthog/api/services/query.py b/posthog/api/services/query.py index b5ca456ee985e..aaca464af51a7 100644 --- a/posthog/api/services/query.py +++ b/posthog/api/services/query.py @@ -30,7 +30,7 @@ QUERY_WITH_RUNNER_NO_CACHE = [ "HogQLQuery", "EventsQuery", - "PersonsQuery", + "ActorsQuery", "SessionsTimelineQuery", ] diff --git a/posthog/api/shared.py b/posthog/api/shared.py index 1a497278b68d0..0ea32d6582330 100644 --- a/posthog/api/shared.py +++ b/posthog/api/shared.py @@ -18,6 +18,7 @@ class Meta: "uuid", "distinct_id", "first_name", + "last_name", "email", "is_email_verified", ] diff --git a/posthog/api/test/__snapshots__/test_insight.ambr b/posthog/api/test/__snapshots__/test_insight.ambr index 3fe43694ef8c1..6c6c7ca4b888a 100644 --- a/posthog/api/test/__snapshots__/test_insight.ambr +++ b/posthog/api/test/__snapshots__/test_insight.ambr @@ -25,7 +25,7 @@ AND (and(ifNull(less(toInt64OrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'int_value'), ''), 'null'), '^"|"$', '')), 10), 0), 1)) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -258,7 +258,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -319,7 +319,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- diff --git a/posthog/api/test/dashboards/test_dashboard_text_tiles.py b/posthog/api/test/dashboards/test_dashboard_text_tiles.py index 509c7b0b9f36a..3bf802f18be6b 100644 --- a/posthog/api/test/dashboards/test_dashboard_text_tiles.py +++ b/posthog/api/test/dashboards/test_dashboard_text_tiles.py @@ -24,6 +24,7 @@ def _serialised_user(user: Optional[User]) -> Optional[Dict[str, Optional[Union[ "distinct_id": user.distinct_id, "email": user.email, "first_name": "", + "last_name": "", "id": user.id, "uuid": str(user.uuid), "is_email_verified": None, diff --git a/posthog/api/test/test_cohort.py b/posthog/api/test/test_cohort.py index f9e03897b599f..33128b639d03f 100644 --- a/posthog/api/test/test_cohort.py +++ b/posthog/api/test/test_cohort.py @@ -725,7 +725,7 @@ def test_creating_update_and_calculating_with_new_cohort_query(self, patch_captu "name": "cohort A", "is_static": True, "query": { - "kind": "PersonsQuery", + "kind": "ActorsQuery", "properties": [ { "key": "$some_prop", @@ -755,7 +755,7 @@ def test_creating_update_and_calculating_with_new_cohort_query_dynamic_error(sel data={ "name": "cohort A", "query": { - "kind": "PersonsQuery", + "kind": "ActorsQuery", "properties": [ { "key": "$some_prop", diff --git a/posthog/api/test/test_insight.py b/posthog/api/test/test_insight.py index c3ddb7e3b5dfd..5710513597f6b 100644 --- a/posthog/api/test/test_insight.py +++ b/posthog/api/test/test_insight.py @@ -76,6 +76,7 @@ def test_created_updated_and_last_modified(self) -> None: "uuid": str(self.user.uuid), "distinct_id": self.user.distinct_id, "first_name": self.user.first_name, + "last_name": self.user.last_name, "email": self.user.email, "is_email_verified": None, } @@ -84,6 +85,7 @@ def test_created_updated_and_last_modified(self) -> None: "uuid": str(alt_user.uuid), "distinct_id": alt_user.distinct_id, "first_name": alt_user.first_name, + "last_name": alt_user.last_name, "email": alt_user.email, "is_email_verified": None, } diff --git a/posthog/api/test/test_organization_feature_flag.py b/posthog/api/test/test_organization_feature_flag.py index 78e72269b20bb..90576b688aa75 100644 --- a/posthog/api/test/test_organization_feature_flag.py +++ b/posthog/api/test/test_organization_feature_flag.py @@ -50,6 +50,7 @@ def test_get_feature_flag_success(self): "uuid": str(self.user.uuid), "distinct_id": self.user.distinct_id, "first_name": self.user.first_name, + "last_name": self.user.last_name, "email": self.user.email, "is_email_verified": self.user.is_email_verified, }, diff --git a/posthog/api/test/test_organization_invites.py b/posthog/api/test/test_organization_invites.py index 0e52252781963..6d8c9d3a11fc5 100644 --- a/posthog/api/test/test_organization_invites.py +++ b/posthog/api/test/test_organization_invites.py @@ -88,6 +88,7 @@ def test_add_organization_invite_with_email(self, mock_capture): "distinct_id": self.user.distinct_id, "email": self.user.email, "first_name": self.user.first_name, + "last_name": self.user.last_name, "is_email_verified": self.user.is_email_verified, }, "is_expired": False, diff --git a/posthog/api/test/test_organization_members.py b/posthog/api/test/test_organization_members.py index 2416e5552fa9a..132f9a5c4ebe8 100644 --- a/posthog/api/test/test_organization_members.py +++ b/posthog/api/test/test_organization_members.py @@ -97,6 +97,7 @@ def test_change_organization_member_level(self): "uuid": str(user.uuid), "distinct_id": str(user.distinct_id), "first_name": user.first_name, + "last_name": user.last_name, "email": user.email, "is_email_verified": None, }, diff --git a/posthog/api/test/test_scheduled_change.py b/posthog/api/test/test_scheduled_change.py index f7ec88f204c82..415e987cb0216 100644 --- a/posthog/api/test/test_scheduled_change.py +++ b/posthog/api/test/test_scheduled_change.py @@ -13,7 +13,7 @@ def test_can_create_flag_change(self): f"/api/projects/{self.team.id}/scheduled_changes/", data={ "id": 6, - "record_id": 119, + "record_id": "119", "model_name": "FeatureFlag", "payload": payload, "scheduled_at": "2023-12-08T12:00:00Z", @@ -27,6 +27,6 @@ def test_can_create_flag_change(self): assert response.status_code == status.HTTP_201_CREATED, response_data assert ScheduledChange.objects.filter(id=response_data["id"]).exists() assert response_data["model_name"] == "FeatureFlag" - assert response_data["record_id"] == 119 + assert response_data["record_id"] == "119" assert response_data["payload"] == payload assert response_data["created_by"]["id"] == self.user.id diff --git a/posthog/api/test/test_signup.py b/posthog/api/test/test_signup.py index 00c101e4487ee..d4e71415b4569 100644 --- a/posthog/api/test/test_signup.py +++ b/posthog/api/test/test_signup.py @@ -62,6 +62,7 @@ def test_api_sign_up(self, mock_capture): "id": user.pk, "uuid": str(user.uuid), "distinct_id": user.distinct_id, + "last_name": "", "first_name": "John", "email": "hedgehog@posthog.com", "redirect_url": "/", @@ -210,6 +211,7 @@ def test_signup_minimum_attrs(self, mock_capture): "id": user.pk, "uuid": str(user.uuid), "distinct_id": user.distinct_id, + "last_name": "", "first_name": "Jane", "email": "hedgehog2@posthog.com", "redirect_url": "/", @@ -364,6 +366,7 @@ def test_default_dashboard_is_created_on_signup(self): "id": user.pk, "uuid": str(user.uuid), "distinct_id": user.distinct_id, + "last_name": "", "first_name": "Jane", "email": "hedgehog75@posthog.com", "redirect_url": "/", @@ -868,6 +871,7 @@ def test_api_invite_sign_up(self, mock_capture): "id": user.pk, "uuid": str(user.uuid), "distinct_id": user.distinct_id, + "last_name": "", "first_name": "Alice", "email": "test+99@posthog.com", "redirect_url": "/", @@ -1076,6 +1080,7 @@ def test_existing_user_can_sign_up_to_a_new_organization(self, mock_update_disti "id": user.pk, "uuid": str(user.uuid), "distinct_id": user.distinct_id, + "last_name": "", "first_name": "", "email": "test+159@posthog.com", "redirect_url": "/", @@ -1151,6 +1156,7 @@ def test_cannot_use_claim_invite_endpoint_to_update_user(self, mock_capture): "id": user.pk, "uuid": str(user.uuid), "distinct_id": user.distinct_id, + "last_name": "", "first_name": "", "email": "test+189@posthog.com", "redirect_url": "/", diff --git a/posthog/api/user.py b/posthog/api/user.py index c7c1813b8b38f..26e887237906b 100644 --- a/posthog/api/user.py +++ b/posthog/api/user.py @@ -89,6 +89,7 @@ class Meta: "uuid", "distinct_id", "first_name", + "last_name", "email", "pending_email", "email_opt_in", diff --git a/posthog/celery.py b/posthog/celery.py index d1804524760ac..7980df823600b 100644 --- a/posthog/celery.py +++ b/posthog/celery.py @@ -271,6 +271,13 @@ def setup_periodic_tasks(sender: Celery, **kwargs): name="recalculate cohorts", ) + add_periodic_task_with_expiry( + sender, + 120, + process_scheduled_changes.s(), + name="process scheduled changes", + ) + if clear_clickhouse_crontab := get_crontab(settings.CLEAR_CLICKHOUSE_REMOVED_DATA_SCHEDULE_CRON): sender.add_periodic_task( clear_clickhouse_crontab, @@ -871,6 +878,13 @@ def calculate_cohort(): calculate_cohorts() +@app.task(ignore_result=True) +def process_scheduled_changes(): + from posthog.tasks.process_scheduled_changes import process_scheduled_changes + + process_scheduled_changes() + + @app.task(ignore_result=True) def sync_insight_cache_states_task(): from posthog.caching.insight_caching_state import sync_insight_cache_states diff --git a/posthog/hogql/test/_test_parser.py b/posthog/hogql/test/_test_parser.py index 16d4654397088..1071f10aa8a0f 100644 --- a/posthog/hogql/test/_test_parser.py +++ b/posthog/hogql/test/_test_parser.py @@ -1561,7 +1561,7 @@ def test_visit_hogqlx_tag_alias(self): def test_visit_hogqlx_tag_source(self): query = """ select id, email from ( - @@ -1572,7 +1572,7 @@ def test_visit_hogqlx_tag_source(self): node = self._select(query) table_node = cast(ast.SelectQuery, node).select_from.table assert table_node == ast.HogQLXTag( - kind="PersonsQuery", + kind="ActorsQuery", attributes=[ ast.HogQLXAttribute( name="select", diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py index 266a8cdeb65cc..1a7b2f6b7f99a 100644 --- a/posthog/hogql/test/test_resolver.py +++ b/posthog/hogql/test/test_resolver.py @@ -338,7 +338,7 @@ def test_visit_hogqlx_tag_alias(self): def test_visit_hogqlx_tag_source(self): query = """ select id, email from ( - diff --git a/posthog/hogql_queries/actor_strategies.py b/posthog/hogql_queries/actor_strategies.py index 466b52ff18d79..747c7e15da362 100644 --- a/posthog/hogql_queries/actor_strategies.py +++ b/posthog/hogql_queries/actor_strategies.py @@ -6,7 +6,7 @@ from posthog.hogql.property import property_to_expr from posthog.hogql_queries.insights.paginators import HogQLHasMorePaginator from posthog.models import Team, Person, Group -from posthog.schema import PersonsQuery +from posthog.schema import ActorsQuery class ActorStrategy: @@ -14,7 +14,7 @@ class ActorStrategy: origin: str origin_id: str - def __init__(self, team: Team, query: PersonsQuery, paginator: HogQLHasMorePaginator): + def __init__(self, team: Team, query: ActorsQuery, paginator: HogQLHasMorePaginator): self.team = team self.paginator = paginator self.query = query diff --git a/posthog/hogql_queries/persons_query_runner.py b/posthog/hogql_queries/actors_query_runner.py similarity index 93% rename from posthog/hogql_queries/persons_query_runner.py rename to posthog/hogql_queries/actors_query_runner.py index 2b97a21811f65..b0fd3b96c1ae3 100644 --- a/posthog/hogql_queries/persons_query_runner.py +++ b/posthog/hogql_queries/actors_query_runner.py @@ -4,15 +4,15 @@ from posthog.hogql.parser import parse_expr, parse_order_expr from posthog.hogql.property import has_aggregation from posthog.hogql_queries.actor_strategies import ActorStrategy, PersonStrategy, GroupStrategy -from posthog.hogql_queries.insights.insight_persons_query_runner import InsightPersonsQueryRunner +from posthog.hogql_queries.insights.insight_actors_query_runner import InsightActorsQueryRunner from posthog.hogql_queries.insights.paginators import HogQLHasMorePaginator from posthog.hogql_queries.query_runner import QueryRunner, get_query_runner -from posthog.schema import PersonsQuery, PersonsQueryResponse +from posthog.schema import ActorsQuery, ActorsQueryResponse -class PersonsQueryRunner(QueryRunner): - query: PersonsQuery - query_type = PersonsQuery +class ActorsQueryRunner(QueryRunner): + query: ActorsQuery + query_type = ActorsQuery def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -28,7 +28,7 @@ def __init__(self, *args, **kwargs): @property def group_type_index(self) -> int | None: - if not self.source_query_runner or not isinstance(self.source_query_runner, InsightPersonsQueryRunner): + if not self.source_query_runner or not isinstance(self.source_query_runner, InsightActorsQueryRunner): return None return self.source_query_runner.group_type_index @@ -46,9 +46,9 @@ def enrich_with_actors(self, results, actor_column_index, actors_lookup) -> Gene new_row[actor_column_index] = actor if actor else {"id": actor_id} yield new_row - def calculate(self) -> PersonsQueryResponse: + def calculate(self) -> ActorsQueryResponse: response = self.paginator.execute_hogql_query( - query_type="PersonsQuery", + query_type="ActorsQuery", query=self.to_query(), team=self.team, timings=self.timings, @@ -65,7 +65,7 @@ def calculate(self) -> PersonsQueryResponse: missing_actors_count = len(self.paginator.results) - len(actors_lookup) results = self.enrich_with_actors(results, input_columns.index(column_name), actors_lookup) - return PersonsQueryResponse( + return ActorsQueryResponse( results=results, timings=response.timings, types=[t for _, t in response.types] if response.types else None, @@ -90,7 +90,7 @@ def source_id_column(self, source_query: ast.SelectQuery) -> List[str]: def source_table_join(self) -> ast.JoinExpr: assert self.source_query_runner is not None # For type checking - source_query = self.source_query_runner.to_persons_query() + source_query = self.source_query_runner.to_actors_query() source_id_chain = self.source_id_column(source_query) source_alias = "source" @@ -182,7 +182,7 @@ def to_query(self) -> ast.SelectQuery: return stmt - def to_persons_query(self) -> ast.SelectQuery: + def to_actors_query(self) -> ast.SelectQuery: return self.to_query() def _is_stale(self, cached_result_package): diff --git a/posthog/hogql_queries/hogql_query_runner.py b/posthog/hogql_queries/hogql_query_runner.py index 1a6bcc89c730c..853022c266aa0 100644 --- a/posthog/hogql_queries/hogql_query_runner.py +++ b/posthog/hogql_queries/hogql_query_runner.py @@ -37,7 +37,7 @@ def to_query(self) -> ast.SelectQuery: parsed_select = replace_filters(parsed_select, self.query.filters, self.team) return parsed_select - def to_persons_query(self) -> ast.SelectQuery: + def to_actors_query(self) -> ast.SelectQuery: return self.to_query() def calculate(self) -> HogQLQueryResponse: diff --git a/posthog/hogql_queries/insights/insight_persons_query_runner.py b/posthog/hogql_queries/insights/insight_actors_query_runner.py similarity index 79% rename from posthog/hogql_queries/insights/insight_persons_query_runner.py rename to posthog/hogql_queries/insights/insight_actors_query_runner.py index de14b029cc1ac..4a5c437824d7e 100644 --- a/posthog/hogql_queries/insights/insight_persons_query_runner.py +++ b/posthog/hogql_queries/insights/insight_actors_query_runner.py @@ -8,12 +8,12 @@ from posthog.hogql_queries.insights.trends.trends_query_runner import TrendsQueryRunner from posthog.hogql_queries.query_runner import QueryRunner, get_query_runner from posthog.models.filters.mixins.utils import cached_property -from posthog.schema import InsightPersonsQuery, HogQLQueryResponse +from posthog.schema import InsightActorsQuery, HogQLQueryResponse -class InsightPersonsQueryRunner(QueryRunner): - query: InsightPersonsQuery - query_type = InsightPersonsQuery +class InsightActorsQueryRunner(QueryRunner): + query: InsightActorsQuery + query_type = InsightActorsQuery @cached_property def source_runner(self) -> QueryRunner: @@ -24,17 +24,17 @@ def to_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: lifecycle_runner = cast(LifecycleQueryRunner, self.source_runner) day = self.query.day status = self.query.status - return lifecycle_runner.to_persons_query(day=day, status=status) + return lifecycle_runner.to_actors_query(day=day, status=status) elif isinstance(self.source_runner, TrendsQueryRunner): trends_runner = cast(TrendsQueryRunner, self.source_runner) - return trends_runner.to_persons_query() + return trends_runner.to_actors_query() elif isinstance(self.source_runner, RetentionQueryRunner): retention_runner = cast(RetentionQueryRunner, self.source_runner) - return retention_runner.to_persons_query(interval=self.query.interval) + return retention_runner.to_actors_query(interval=self.query.interval) raise ValueError(f"Cannot convert source query of type {self.query.source.kind} to persons query") - def to_persons_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: + def to_actors_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: return self.to_query() @property @@ -46,7 +46,7 @@ def group_type_index(self) -> int | None: def calculate(self) -> HogQLQueryResponse: return execute_hogql_query( - query_type="InsightPersonsQuery", + query_type="InsightActorsQuery", query=self.to_query(), team=self.team, timings=self.timings, diff --git a/posthog/hogql_queries/insights/lifecycle_query_runner.py b/posthog/hogql_queries/insights/lifecycle_query_runner.py index 92cf4e23704cb..2657b1cebc6a6 100644 --- a/posthog/hogql_queries/insights/lifecycle_query_runner.py +++ b/posthog/hogql_queries/insights/lifecycle_query_runner.py @@ -90,7 +90,7 @@ def to_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: ) return lifecycle_query - def to_persons_query( + def to_actors_query( self, day: Optional[str] = None, status: Optional[str] = None ) -> ast.SelectQuery | ast.SelectUnionQuery: with self.timings.measure("persons_query"): diff --git a/posthog/hogql_queries/insights/retention_query_runner.py b/posthog/hogql_queries/insights/retention_query_runner.py index 1920d70ee3d5e..0a2c7abb117c4 100644 --- a/posthog/hogql_queries/insights/retention_query_runner.py +++ b/posthog/hogql_queries/insights/retention_query_runner.py @@ -329,7 +329,7 @@ def calculate(self) -> RetentionQueryResponse: return RetentionQueryResponse(results=results, timings=response.timings, hogql=hogql) - def to_persons_query(self, interval: Optional[int] = None) -> ast.SelectQuery: + def to_actors_query(self, interval: Optional[int] = None) -> ast.SelectQuery: with self.timings.measure("retention_query"): retention_query = parse_select( """ diff --git a/posthog/hogql_queries/insights/test/test_insight_persons_query_runner.py b/posthog/hogql_queries/insights/test/test_insight_actors_query_runner.py similarity index 85% rename from posthog/hogql_queries/insights/test/test_insight_persons_query_runner.py rename to posthog/hogql_queries/insights/test/test_insight_actors_query_runner.py index 61afd1c3eae30..933c8fdf85114 100644 --- a/posthog/hogql_queries/insights/test/test_insight_persons_query_runner.py +++ b/posthog/hogql_queries/insights/test/test_insight_actors_query_runner.py @@ -13,7 +13,7 @@ ) -class TestInsightPersonsQueryRunner(ClickhouseTestMixin, APIBaseTest): +class TestInsightActorsQueryRunner(ClickhouseTestMixin, APIBaseTest): maxDiff = None def _create_events(self, data, event="$pageview"): @@ -72,14 +72,14 @@ def test_insight_persons_lifecycle_query(self): response = self.select( """ select * from ( - - + + } series={[]} /> - - + + ) """, {"date_from": ast.Constant(value=date_from), "date_to": ast.Constant(value=date_to)}, @@ -99,15 +99,15 @@ def test_insight_persons_lifecycle_query_week_monday(self): response = self.select( """ select * from ( - - + + } series={[]} /> - - + + ) """, {"date_from": ast.Constant(value=date_from), "date_to": ast.Constant(value=date_to)}, @@ -127,15 +127,15 @@ def test_insight_persons_lifecycle_query_week_sunday(self): response = self.select( """ select * from ( - - + + } series={[]} /> - - + + ) """, {"date_from": ast.Constant(value=date_from), "date_to": ast.Constant(value=date_to)}, diff --git a/posthog/hogql_queries/insights/test/test_paginators.py b/posthog/hogql_queries/insights/test/test_paginators.py index 7954f35daf7f2..ac83efb45b353 100644 --- a/posthog/hogql_queries/insights/test/test_paginators.py +++ b/posthog/hogql_queries/insights/test/test_paginators.py @@ -6,10 +6,10 @@ ) from posthog.hogql.parser import parse_select from posthog.hogql_queries.insights.paginators import HogQLHasMorePaginator -from posthog.hogql_queries.persons_query_runner import PersonsQueryRunner +from posthog.hogql_queries.actors_query_runner import ActorsQueryRunner from posthog.models.utils import UUIDT from posthog.schema import ( - PersonsQuery, + ActorsQuery, PersonPropertyFilter, PropertyOperator, ) @@ -49,8 +49,8 @@ def _create_random_persons(self) -> str: flush_persons_and_events() return random_uuid - def _create_runner(self, query: PersonsQuery) -> PersonsQueryRunner: - return PersonsQueryRunner(team=self.team, query=query) + def _create_runner(self, query: ActorsQuery) -> ActorsQueryRunner: + return ActorsQueryRunner(team=self.team, query=query) def setUp(self): super().setUp() @@ -58,14 +58,14 @@ def setUp(self): def test_persons_query_limit(self): runner = self._create_runner( - PersonsQuery(select=["properties.email"], orderBy=["properties.email DESC"], limit=1) + ActorsQuery(select=["properties.email"], orderBy=["properties.email DESC"], limit=1) ) response = runner.calculate() self.assertEqual(response.results, [[f"jacob9@{self.random_uuid}.posthog.com"]]) self.assertEqual(response.hasMore, True) runner = self._create_runner( - PersonsQuery( + ActorsQuery( select=["properties.email"], orderBy=["properties.email DESC"], limit=1, @@ -78,7 +78,7 @@ def test_persons_query_limit(self): def test_zero_limit(self): """Test behavior with limit set to zero.""" - runner = self._create_runner(PersonsQuery(select=["properties.email"], limit=0)) + runner = self._create_runner(ActorsQuery(select=["properties.email"], limit=0)) response = runner.calculate() self.assertEqual(runner.paginator.limit, 100) self.assertEqual(response.limit, 100) @@ -87,7 +87,7 @@ def test_zero_limit(self): def test_negative_limit(self): """Test behavior with negative limit value.""" - runner = self._create_runner(PersonsQuery(select=["properties.email"], limit=-1)) + runner = self._create_runner(ActorsQuery(select=["properties.email"], limit=-1)) response = runner.calculate() self.assertEqual(runner.paginator.limit, 100) self.assertEqual(response.limit, 100) @@ -96,7 +96,7 @@ def test_negative_limit(self): def test_exact_limit_match(self): """Test when available items equal the limit.""" - runner = self._create_runner(PersonsQuery(select=["properties.email"], limit=10)) + runner = self._create_runner(ActorsQuery(select=["properties.email"], limit=10)) response = runner.calculate() self.assertEqual(len(response.results), 10) self.assertFalse(response.hasMore) @@ -104,7 +104,7 @@ def test_exact_limit_match(self): def test_empty_result_set(self): """Test behavior when query returns no results.""" runner = self._create_runner( - PersonsQuery( + ActorsQuery( select=["properties.email"], limit=10, properties=[ @@ -119,14 +119,14 @@ def test_empty_result_set(self): def test_large_offset(self): """Test behavior with offset larger than the total number of items.""" self.random_uuid = self._create_random_persons() - runner = self._create_runner(PersonsQuery(select=["properties.email"], limit=5, offset=100)) + runner = self._create_runner(ActorsQuery(select=["properties.email"], limit=5, offset=100)) response = runner.calculate() self.assertEqual(len(response.results), 0) self.assertFalse(response.hasMore) def test_offset_plus_limit_exceeding_total(self): """Test when sum of offset and limit exceeds total items.""" - runner = self._create_runner(PersonsQuery(select=["properties.email"], limit=10, offset=5)) + runner = self._create_runner(ActorsQuery(select=["properties.email"], limit=10, offset=5)) response = runner.calculate() self.assertEqual(runner.paginator.offset, 5) self.assertEqual(len(response.results), 5) diff --git a/posthog/hogql_queries/insights/test/test_retention_query_runner.py b/posthog/hogql_queries/insights/test/test_retention_query_runner.py index eacf27a5021bf..999c93f57e71e 100644 --- a/posthog/hogql_queries/insights/test/test_retention_query_runner.py +++ b/posthog/hogql_queries/insights/test/test_retention_query_runner.py @@ -11,7 +11,7 @@ TREND_FILTER_TYPE_EVENTS, ) from posthog.hogql_queries.insights.retention_query_runner import RetentionQueryRunner -from posthog.hogql_queries.persons_query_runner import PersonsQueryRunner +from posthog.hogql_queries.actors_query_runner import ActorsQueryRunner from posthog.models import Action, ActionStep from posthog.test.base import ( APIBaseTest, @@ -74,13 +74,13 @@ def run_actors_query(self, interval, query): query["kind"] = "RetentionQuery" if not query.get("retentionFilter"): query["retentionFilter"] = {} - runner = PersonsQueryRunner( + runner = ActorsQueryRunner( team=self.team, query={ "select": ["person", "appearances"], "orderBy": ["length(appearances) DESC", "actor_id"], "source": { - "kind": "InsightPersonsQuery", + "kind": "InsightActorsQuery", "interval": interval, "source": query, }, diff --git a/posthog/hogql_queries/insights/trends/trends_query_runner.py b/posthog/hogql_queries/insights/trends/trends_query_runner.py index 25ca4a8870457..a2fd1b49bd0fc 100644 --- a/posthog/hogql_queries/insights/trends/trends_query_runner.py +++ b/posthog/hogql_queries/insights/trends/trends_query_runner.py @@ -98,7 +98,7 @@ def to_query(self) -> List[ast.SelectQuery]: return queries - def to_persons_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: + def to_actors_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: queries = [] with self.timings.measure("trends_persons_query"): for series in self.series: diff --git a/posthog/hogql_queries/query_runner.py b/posthog/hogql_queries/query_runner.py index afe2b0aa84544..6dbb93b661fef 100644 --- a/posthog/hogql_queries/query_runner.py +++ b/posthog/hogql_queries/query_runner.py @@ -23,11 +23,11 @@ LifecycleQuery, WebTopClicksQuery, WebOverviewQuery, - PersonsQuery, + ActorsQuery, EventsQuery, WebStatsTableQuery, HogQLQuery, - InsightPersonsQuery, + InsightActorsQuery, DashboardFilter, HogQLQueryModifiers, RetentionQuery, @@ -78,9 +78,9 @@ class CachedQueryResponse(QueryResponse): HogQLQuery, TrendsQuery, LifecycleQuery, - InsightPersonsQuery, + InsightActorsQuery, EventsQuery, - PersonsQuery, + ActorsQuery, RetentionQuery, SessionsTimelineQuery, WebOverviewQuery, @@ -144,21 +144,21 @@ def get_query_runner( limit_context=limit_context, modifiers=modifiers, ) - if kind == "PersonsQuery": - from .persons_query_runner import PersonsQueryRunner + if kind == "ActorsQuery": + from .actors_query_runner import ActorsQueryRunner - return PersonsQueryRunner( - query=cast(PersonsQuery | Dict[str, Any], query), + return ActorsQueryRunner( + query=cast(ActorsQuery | Dict[str, Any], query), team=team, timings=timings, limit_context=limit_context, modifiers=modifiers, ) - if kind == "InsightPersonsQuery": - from .insights.insight_persons_query_runner import InsightPersonsQueryRunner + if kind == "InsightActorsQuery": + from .insights.insight_actors_query_runner import InsightActorsQueryRunner - return InsightPersonsQueryRunner( - query=cast(InsightPersonsQuery | Dict[str, Any], query), + return InsightActorsQueryRunner( + query=cast(InsightActorsQuery | Dict[str, Any], query), team=team, timings=timings, limit_context=limit_context, @@ -263,7 +263,7 @@ def run(self, refresh_requested: Optional[bool] = None) -> CachedQueryResponse: def to_query(self) -> ast.SelectQuery: raise NotImplementedError() - def to_persons_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: + def to_actors_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: # TODO: add support for selecting and filtering by breakdowns raise NotImplementedError() diff --git a/posthog/hogql_queries/sessions_timeline_query_runner.py b/posthog/hogql_queries/sessions_timeline_query_runner.py index 54f024900ff06..d920ec7cf94fd 100644 --- a/posthog/hogql_queries/sessions_timeline_query_runner.py +++ b/posthog/hogql_queries/sessions_timeline_query_runner.py @@ -122,7 +122,7 @@ def to_query(self) -> ast.SelectQuery: ) return cast(ast.SelectQuery, select_query) - def to_persons_query(self): + def to_actors_query(self): return parse_select( """SELECT DISTINCT person_id FROM {events_subquery}""", {"events_subquery": self._get_events_subquery()} ) diff --git a/posthog/hogql_queries/test/test_persons_query_runner.py b/posthog/hogql_queries/test/test_actors_query_runner.py similarity index 83% rename from posthog/hogql_queries/test/test_persons_query_runner.py rename to posthog/hogql_queries/test/test_actors_query_runner.py index e25bf67166b81..da9ea4ff35382 100644 --- a/posthog/hogql_queries/test/test_persons_query_runner.py +++ b/posthog/hogql_queries/test/test_actors_query_runner.py @@ -1,9 +1,9 @@ from posthog.hogql import ast from posthog.hogql.visitor import clear_locations -from posthog.hogql_queries.persons_query_runner import PersonsQueryRunner +from posthog.hogql_queries.actors_query_runner import ActorsQueryRunner from posthog.models.utils import UUIDT from posthog.schema import ( - PersonsQuery, + ActorsQuery, PersonPropertyFilter, HogQLPropertyFilter, PropertyOperator, @@ -12,7 +12,7 @@ DateRange, EventsNode, IntervalType, - InsightPersonsQuery, + InsightActorsQuery, ) from posthog.test.base import ( APIBaseTest, @@ -25,7 +25,7 @@ from django.test import override_settings -class TestPersonsQueryRunner(ClickhouseTestMixin, APIBaseTest): +class TestActorsQueryRunner(ClickhouseTestMixin, APIBaseTest): maxDiff = None random_uuid: str @@ -52,15 +52,15 @@ def _create_random_persons(self) -> str: flush_persons_and_events() return random_uuid - def _create_runner(self, query: PersonsQuery) -> PersonsQueryRunner: - return PersonsQueryRunner(team=self.team, query=query) + def _create_runner(self, query: ActorsQuery) -> ActorsQueryRunner: + return ActorsQueryRunner(team=self.team, query=query) def setUp(self): super().setUp() def test_default_persons_query(self): self.random_uuid = self._create_random_persons() - runner = self._create_runner(PersonsQuery()) + runner = self._create_runner(ActorsQuery()) query = runner.to_query() query = clear_locations(query) @@ -86,7 +86,7 @@ def test_default_persons_query(self): def test_persons_query_properties(self): self.random_uuid = self._create_random_persons() runner = self._create_runner( - PersonsQuery( + ActorsQuery( properties=[ PersonPropertyFilter( key="random_uuid", @@ -102,7 +102,7 @@ def test_persons_query_properties(self): def test_persons_query_fixed_properties(self): self.random_uuid = self._create_random_persons() runner = self._create_runner( - PersonsQuery( + ActorsQuery( fixedProperties=[ PersonPropertyFilter( key="random_uuid", @@ -118,55 +118,55 @@ def test_persons_query_fixed_properties(self): def test_persons_query_search_email(self): self.random_uuid = self._create_random_persons() self._create_random_persons() - runner = self._create_runner(PersonsQuery(search=f"jacob4@{self.random_uuid}.posthog")) + runner = self._create_runner(ActorsQuery(search=f"jacob4@{self.random_uuid}.posthog")) self.assertEqual(len(runner.calculate().results), 1) - runner = self._create_runner(PersonsQuery(search=f"JACOB4@{self.random_uuid}.posthog")) + runner = self._create_runner(ActorsQuery(search=f"JACOB4@{self.random_uuid}.posthog")) self.assertEqual(len(runner.calculate().results), 1) def test_persons_query_search_name(self): self.random_uuid = self._create_random_persons() - runner = self._create_runner(PersonsQuery(search=f"Mr Jacob {self.random_uuid}")) + runner = self._create_runner(ActorsQuery(search=f"Mr Jacob {self.random_uuid}")) self.assertEqual(len(runner.calculate().results), 10) - runner = self._create_runner(PersonsQuery(search=f"MR JACOB {self.random_uuid}")) + runner = self._create_runner(ActorsQuery(search=f"MR JACOB {self.random_uuid}")) self.assertEqual(len(runner.calculate().results), 10) def test_persons_query_search_distinct_id(self): self.random_uuid = self._create_random_persons() - runner = self._create_runner(PersonsQuery(search=f"id-{self.random_uuid}-9")) + runner = self._create_runner(ActorsQuery(search=f"id-{self.random_uuid}-9")) self.assertEqual(len(runner.calculate().results), 1) - runner = self._create_runner(PersonsQuery(search=f"id-{self.random_uuid}-9")) + runner = self._create_runner(ActorsQuery(search=f"id-{self.random_uuid}-9")) self.assertEqual(len(runner.calculate().results), 1) def test_persons_query_aggregation_select_having(self): self.random_uuid = self._create_random_persons() - runner = self._create_runner(PersonsQuery(select=["properties.name", "count()"])) + runner = self._create_runner(ActorsQuery(select=["properties.name", "count()"])) results = runner.calculate().results self.assertEqual(results, [[f"Mr Jacob {self.random_uuid}", 10]]) def test_persons_query_order_by(self): self.random_uuid = self._create_random_persons() - runner = self._create_runner(PersonsQuery(select=["properties.email"], orderBy=["properties.email DESC"])) + runner = self._create_runner(ActorsQuery(select=["properties.email"], orderBy=["properties.email DESC"])) results = runner.calculate().results self.assertEqual(results[0], [f"jacob9@{self.random_uuid}.posthog.com"]) def test_persons_query_order_by_with_aliases(self): # We use the first column by default as an order key. It used to cause "error redefining alias" errors. self.random_uuid = self._create_random_persons() - runner = self._create_runner(PersonsQuery(select=["properties.email as email"])) + runner = self._create_runner(ActorsQuery(select=["properties.email as email"])) results = runner.calculate().results self.assertEqual(results[0], [f"jacob0@{self.random_uuid}.posthog.com"]) def test_persons_query_limit(self): self.random_uuid = self._create_random_persons() runner = self._create_runner( - PersonsQuery(select=["properties.email"], orderBy=["properties.email DESC"], limit=1) + ActorsQuery(select=["properties.email"], orderBy=["properties.email DESC"], limit=1) ) response = runner.calculate() self.assertEqual(response.results, [[f"jacob9@{self.random_uuid}.posthog.com"]]) self.assertEqual(response.hasMore, True) runner = self._create_runner( - PersonsQuery( + ActorsQuery( select=["properties.email"], orderBy=["properties.email DESC"], limit=1, @@ -181,7 +181,7 @@ def test_persons_query_limit(self): def test_source_hogql_query_poe_on(self): self.random_uuid = self._create_random_persons() source_query = HogQLQuery(query="SELECT distinct person_id FROM events WHERE event='clicky-4'") - query = PersonsQuery( + query = ActorsQuery( select=["properties.email"], orderBy=["properties.email DESC"], source=source_query, @@ -194,7 +194,7 @@ def test_source_hogql_query_poe_on(self): def test_source_hogql_query_poe_off(self): self.random_uuid = self._create_random_persons() source_query = HogQLQuery(query="SELECT distinct person_id FROM events WHERE event='clicky-4'") - query = PersonsQuery( + query = ActorsQuery( select=["properties.email"], orderBy=["properties.email DESC"], source=source_query, @@ -219,10 +219,10 @@ def test_source_lifecycle_query(self): interval=IntervalType.day, dateRange=DateRange(date_from="-7d"), ) - query = PersonsQuery( + query = ActorsQuery( select=["properties.email"], orderBy=["properties.email DESC"], - source=InsightPersonsQuery(source=source_query), + source=InsightActorsQuery(source=source_query), ) runner = self._create_runner(query) response = runner.calculate() diff --git a/posthog/migrations/0379_alter_scheduledchange.py b/posthog/migrations/0379_alter_scheduledchange.py new file mode 100644 index 0000000000000..0e0025324151a --- /dev/null +++ b/posthog/migrations/0379_alter_scheduledchange.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.19 on 2023-12-21 14:01 + +from django.db import migrations, models +from django.contrib.postgres.operations import AddIndexConcurrently # type: ignore + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("posthog", "0378_alter_user_theme_mode"), + ] + + operations = [ + migrations.AlterField( + model_name="scheduledchange", + name="record_id", + field=models.CharField(max_length=200), + ), + migrations.AlterField( + model_name="scheduledchange", + name="scheduled_at", + field=models.DateTimeField(), + ), + AddIndexConcurrently( + model_name="scheduledchange", + index=models.Index(fields=["scheduled_at", "executed_at"], name="posthog_sch_schedul_c3687e_idx"), + ), + ] diff --git a/posthog/models/feature_flag/feature_flag.py b/posthog/models/feature_flag/feature_flag.py index c339abe44d0ed..80c92452d6d74 100644 --- a/posthog/models/feature_flag/feature_flag.py +++ b/posthog/models/feature_flag/feature_flag.py @@ -297,6 +297,31 @@ def get_cohort_ids( return list(cohort_ids) + def scheduled_changes_dispatcher(self, payload): + from posthog.api.feature_flag import FeatureFlagSerializer + + if "operation" not in payload or "value" not in payload: + raise Exception("Invalid payload") + + context = { + "request": {"user": self.created_by}, + "team_id": self.team_id, + } + serializer_data = {} + + if payload["operation"] == "add_release_condition": + existing_groups = self.get_filters().get("groups", []) + new_groups = payload["value"].get("groups", []) + serializer_data["filters"] = {"groups": existing_groups + new_groups} + elif payload["operation"] == "update_status": + serializer_data["active"] = payload["value"] + else: + raise Exception(f"Unrecognized operation: {payload['operation']}") + + serializer = FeatureFlagSerializer(self, data=serializer_data, context=context, partial=True) + if serializer.is_valid(raise_exception=True): + serializer.save() + @property def uses_cohorts(self) -> bool: for condition in self.conditions: diff --git a/posthog/models/scheduled_change.py b/posthog/models/scheduled_change.py index 2fea198fd3ba0..ee92cc59c506e 100644 --- a/posthog/models/scheduled_change.py +++ b/posthog/models/scheduled_change.py @@ -1,5 +1,4 @@ from django.db import models -from django.utils import timezone class ScheduledChange(models.Model): @@ -7,10 +6,10 @@ class AllowedModels(models.TextChoices): FEATURE_FLAG = "FeatureFlag", "feature flag" id = models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID") - record_id = models.IntegerField() + record_id: models.CharField = models.CharField(max_length=200) model_name: models.CharField = models.CharField(max_length=100, choices=AllowedModels.choices) payload: models.JSONField = models.JSONField(default=dict) - scheduled_at: models.DateTimeField = models.DateTimeField(default=timezone.now) + scheduled_at: models.DateTimeField = models.DateTimeField() executed_at: models.DateTimeField = models.DateTimeField(null=True, blank=True) failure_reason = models.CharField(max_length=400, null=True, blank=True) @@ -18,3 +17,8 @@ class AllowedModels(models.TextChoices): created_at: models.DateTimeField = models.DateTimeField(auto_now_add=True) created_by: models.ForeignKey = models.ForeignKey("User", on_delete=models.SET_NULL, null=True) updated_at: models.DateTimeField = models.DateTimeField(auto_now_add=True) + + class Meta: + indexes = [ + models.Index(fields=["scheduled_at", "executed_at"]), + ] diff --git a/posthog/queries/breakdown_props.py b/posthog/queries/breakdown_props.py index fb9132e83398c..059e1998f673e 100644 --- a/posthog/queries/breakdown_props.py +++ b/posthog/queries/breakdown_props.py @@ -50,7 +50,7 @@ def get_breakdown_prop_values( column_optimizer: Optional[ColumnOptimizer] = None, person_properties_mode: PersonPropertiesMode = PersonPropertiesMode.USING_PERSON_PROPERTIES_COLUMN, use_all_funnel_entities: bool = False, -): +) -> Tuple[List[Any], bool]: """ Returns the top N breakdown prop values for event/person breakdown @@ -216,7 +216,7 @@ def get_breakdown_prop_values( elements_query, { "key": filter.breakdown, - "limit": filter.breakdown_limit_or_default, + "limit": filter.breakdown_limit_or_default + 1, "team_id": team.pk, "offset": filter.offset, "timezone": team.timezone, @@ -236,9 +236,11 @@ def get_breakdown_prop_values( ) if filter.using_histogram: - return response[0][0] + return response[0][0], False else: - return [row[0] for row in response] + return [row[0] for row in response[0 : filter.breakdown_limit_or_default]], len( + response + ) > filter.breakdown_limit_or_default def _to_value_expression( diff --git a/posthog/queries/funnels/base.py b/posthog/queries/funnels/base.py index 8ac25880932a7..482e821fd5d11 100644 --- a/posthog/queries/funnels/base.py +++ b/posthog/queries/funnels/base.py @@ -834,7 +834,7 @@ def _get_cohort_breakdown_join(self) -> str: ON events.distinct_id = cohort_join.distinct_id """ - def _get_breakdown_conditions(self) -> Optional[str]: + def _get_breakdown_conditions(self) -> Optional[List[str]]: """ For people, pagination sets the offset param, which is common across filters and gives us the wrong breakdown values here, so we override it. @@ -862,7 +862,7 @@ def _get_breakdown_conditions(self) -> Optional[str]: ): target_entity = self._filter.entities[self._filter.breakdown_attribution_value] - return get_breakdown_prop_values( + values, has_more_values = get_breakdown_prop_values( self._filter, target_entity, "count(*)", @@ -871,6 +871,7 @@ def _get_breakdown_conditions(self) -> Optional[str]: use_all_funnel_entities=use_all_funnel_entities, person_properties_mode=get_person_properties_mode(self._team), ) + return values return None diff --git a/posthog/queries/funnels/test/__snapshots__/test_breakdowns_by_current_url.ambr b/posthog/queries/funnels/test/__snapshots__/test_breakdowns_by_current_url.ambr index d9a0d18dd64b9..66bd37c947545 100644 --- a/posthog/queries/funnels/test/__snapshots__/test_breakdowns_by_current_url.ambr +++ b/posthog/queries/funnels/test/__snapshots__/test_breakdowns_by_current_url.ambr @@ -12,7 +12,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-12 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 100 + LIMIT 101 OFFSET 0 ' --- @@ -104,7 +104,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-12 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 100 + LIMIT 101 OFFSET 0 ' --- diff --git a/posthog/queries/funnels/test/__snapshots__/test_funnel.ambr b/posthog/queries/funnels/test/__snapshots__/test_funnel.ambr index c8a3edd3a92a7..e3d0070518f96 100644 --- a/posthog/queries/funnels/test/__snapshots__/test_funnel.ambr +++ b/posthog/queries/funnels/test/__snapshots__/test_funnel.ambr @@ -1021,7 +1021,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1111,7 +1111,7 @@ AND (has(['xyz'], replaceRegexpAll(JSONExtractRaw(e.properties, '$version'), '^"|"$', ''))) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1204,7 +1204,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- diff --git a/posthog/queries/funnels/test/__snapshots__/test_funnel_strict.ambr b/posthog/queries/funnels/test/__snapshots__/test_funnel_strict.ambr index a7f8077138aee..07f673351ac22 100644 --- a/posthog/queries/funnels/test/__snapshots__/test_funnel_strict.ambr +++ b/posthog/queries/funnels/test/__snapshots__/test_funnel_strict.ambr @@ -10,7 +10,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -97,7 +97,7 @@ AND (has(['xyz'], replaceRegexpAll(JSONExtractRaw(e.properties, '$version'), '^"|"$', ''))) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -187,7 +187,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- diff --git a/posthog/queries/funnels/test/__snapshots__/test_funnel_unordered.ambr b/posthog/queries/funnels/test/__snapshots__/test_funnel_unordered.ambr index d25d5423a41a7..e95f1b894c71b 100644 --- a/posthog/queries/funnels/test/__snapshots__/test_funnel_unordered.ambr +++ b/posthog/queries/funnels/test/__snapshots__/test_funnel_unordered.ambr @@ -10,7 +10,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -26,7 +26,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -159,7 +159,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -175,7 +175,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -316,7 +316,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -332,7 +332,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-08 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- diff --git a/posthog/queries/test/__snapshots__/test_trends.ambr b/posthog/queries/test/__snapshots__/test_trends.ambr index 90570288abc8b..121fd084fb97a 100644 --- a/posthog/queries/test/__snapshots__/test_trends.ambr +++ b/posthog/queries/test/__snapshots__/test_trends.ambr @@ -169,7 +169,7 @@ AND notEmpty(e.person_id) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -279,7 +279,7 @@ AND notEmpty(e.person_id) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -353,7 +353,7 @@ AND (has(['Mac'], replaceRegexpAll(JSONExtractRaw(e.properties, '$os'), '^"|"$', '')))) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -420,7 +420,7 @@ AND (has(['Mac'], replaceRegexpAll(JSONExtractRaw(e.properties, '$os'), '^"|"$', '')))) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -451,7 +451,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-11 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -497,7 +497,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-11 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -563,7 +563,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-12 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -682,7 +682,7 @@ AND (has(['finance'], replaceRegexpAll(JSONExtractRaw(group_properties_0, 'industry'), '^"|"$', ''))) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -780,7 +780,7 @@ AND notEmpty(e.person_id) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -857,7 +857,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -925,7 +925,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1244,7 +1244,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1356,7 +1356,7 @@ AND notEmpty(e.person_id) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1494,7 +1494,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -1593,7 +1593,7 @@ AND notEmpty(e.person_id) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -2257,7 +2257,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-05 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -2459,7 +2459,7 @@ AND toTimeZone(timestamp, 'America/Phoenix') <= toDateTime('2020-01-05 23:59:59', 'America/Phoenix') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -2661,7 +2661,7 @@ AND toTimeZone(timestamp, 'Asia/Tokyo') <= toDateTime('2020-01-05 23:59:59', 'Asia/Tokyo') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -3297,7 +3297,7 @@ OR (has(['val'], replaceRegexpAll(JSONExtractRaw(e.properties, 'key'), '^"|"$', ''))))) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -3407,7 +3407,7 @@ AND ((has(['val'], replaceRegexpAll(JSONExtractRaw(e.properties, 'key'), '^"|"$', '')))) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -3578,7 +3578,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2019-12-31 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -3737,7 +3737,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2019-12-31 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -3850,7 +3850,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -3933,7 +3933,7 @@ AND notEmpty(e.person_id) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -4019,7 +4019,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -4076,7 +4076,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -4308,7 +4308,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-07 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -4516,7 +4516,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -4735,7 +4735,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -4826,7 +4826,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- diff --git a/posthog/queries/trends/breakdown.py b/posthog/queries/trends/breakdown.py index 8d454c8081179..85be3d35f4999 100644 --- a/posthog/queries/trends/breakdown.py +++ b/posthog/queries/trends/breakdown.py @@ -440,7 +440,7 @@ def _breakdown_cohort_params(self): return params, breakdown_filter, breakdown_filter_params, "value" def _breakdown_prop_params(self, aggregate_operation: str, math_params: Dict): - values_arr = get_breakdown_prop_values( + values_arr, has_more_values = get_breakdown_prop_values( self.filter, self.entity, aggregate_operation, @@ -490,7 +490,9 @@ def _breakdown_prop_params(self, aggregate_operation: str, math_params: Dict): return ( { - "values": values_arr, + "values": [*values_arr, breakdown_other_value] + if has_more_values and not self.filter.breakdown_hide_other_aggregation + else values_arr, "breakdown_other_value": breakdown_other_value, "breakdown_null_value": breakdown_null_value, }, diff --git a/posthog/queries/trends/test/__snapshots__/test_breakdowns.ambr b/posthog/queries/trends/test/__snapshots__/test_breakdowns.ambr index de25715bad65c..b8f635bc61459 100644 --- a/posthog/queries/trends/test/__snapshots__/test_breakdowns.ambr +++ b/posthog/queries/trends/test/__snapshots__/test_breakdowns.ambr @@ -152,7 +152,7 @@ AND (sessions.session_duration > 30.0) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -233,7 +233,7 @@ AND (NOT has(['https://test.com'], replaceRegexpAll(JSONExtractRaw(e.properties, '$current_url'), '^"|"$', ''))) GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -450,7 +450,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-12 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -666,7 +666,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-12 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 2 + LIMIT 3 OFFSET 0 ' --- @@ -693,13 +693,13 @@ CROSS JOIN (SELECT breakdown_value FROM - (SELECT [1, 2] as breakdown_value) ARRAY + (SELECT [9007199254740990, 19, 9007199254740991] as breakdown_value) ARRAY JOIN breakdown_value) as sec ORDER BY breakdown_value, day_start UNION ALL SELECT count(*) as total, toStartOfDay(toTimeZone(toDateTime(timestamp, 'UTC'), 'UTC')) as day_start, - transform(ifNull(length(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, '$current_url'), ''), 'null'), '^"|"$', '')), 9007199254740990), ([9007199254740990, 19]), ([9007199254740990, 19]), 9007199254740991) as breakdown_value + transform(ifNull(length(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, '$current_url'), ''), 'null'), '^"|"$', '')), 9007199254740990), ([9007199254740990, 19, 9007199254740991]), ([9007199254740990, 19, 9007199254740991]), 9007199254740991) as breakdown_value FROM events e WHERE e.team_id = 2 AND event = 'watched movie' @@ -727,7 +727,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-12 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 2 + LIMIT 3 OFFSET 0 ' --- @@ -789,7 +789,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-12 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 3 + LIMIT 4 OFFSET 0 ' --- @@ -851,7 +851,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-12 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 2 + LIMIT 3 OFFSET 0 ' --- @@ -878,13 +878,13 @@ CROSS JOIN (SELECT breakdown_value FROM - (SELECT ['$$_posthog_breakdown_null_$$', 'https://example.com'] as breakdown_value) ARRAY + (SELECT ['$$_posthog_breakdown_null_$$', 'https://example.com', '$$_posthog_breakdown_other_$$'] as breakdown_value) ARRAY JOIN breakdown_value) as sec ORDER BY breakdown_value, day_start UNION ALL SELECT count(*) as total, toStartOfDay(toTimeZone(toDateTime(timestamp, 'UTC'), 'UTC')) as day_start, - transform(ifNull(nullIf(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, '$current_url'), ''), 'null'), '^"|"$', ''), ''), '$$_posthog_breakdown_null_$$'), (['$$_posthog_breakdown_null_$$', 'https://example.com']), (['$$_posthog_breakdown_null_$$', 'https://example.com']), '$$_posthog_breakdown_other_$$') as breakdown_value + transform(ifNull(nullIf(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, '$current_url'), ''), 'null'), '^"|"$', ''), ''), '$$_posthog_breakdown_null_$$'), (['$$_posthog_breakdown_null_$$', 'https://example.com', '$$_posthog_breakdown_other_$$']), (['$$_posthog_breakdown_null_$$', 'https://example.com', '$$_posthog_breakdown_other_$$']), '$$_posthog_breakdown_other_$$') as breakdown_value FROM events e WHERE e.team_id = 2 AND event = 'watched movie' @@ -912,7 +912,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-12 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 2 + LIMIT 3 OFFSET 0 ' --- @@ -974,7 +974,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-12 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 3 + LIMIT 4 OFFSET 0 ' --- diff --git a/posthog/queries/trends/test/__snapshots__/test_breakdowns_by_current_url.ambr b/posthog/queries/trends/test/__snapshots__/test_breakdowns_by_current_url.ambr index d02c751ab3a4d..2562ad16d95a4 100644 --- a/posthog/queries/trends/test/__snapshots__/test_breakdowns_by_current_url.ambr +++ b/posthog/queries/trends/test/__snapshots__/test_breakdowns_by_current_url.ambr @@ -12,7 +12,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-12 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -77,7 +77,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-12 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- diff --git a/posthog/queries/trends/test/__snapshots__/test_formula.ambr b/posthog/queries/trends/test/__snapshots__/test_formula.ambr index a86eea7c97d85..d12a680c5d04f 100644 --- a/posthog/queries/trends/test/__snapshots__/test_formula.ambr +++ b/posthog/queries/trends/test/__snapshots__/test_formula.ambr @@ -30,7 +30,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -46,7 +46,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -152,7 +152,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -168,7 +168,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -371,7 +371,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -401,7 +401,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -535,7 +535,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- @@ -551,7 +551,7 @@ AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-04 23:59:59', 'UTC') GROUP BY value ORDER BY count DESC, value DESC - LIMIT 25 + LIMIT 26 OFFSET 0 ' --- diff --git a/posthog/queries/trends/test/test_breakdowns.py b/posthog/queries/trends/test/test_breakdowns.py index 313ef4f37b140..48ed9033c0458 100644 --- a/posthog/queries/trends/test/test_breakdowns.py +++ b/posthog/queries/trends/test/test_breakdowns.py @@ -507,7 +507,7 @@ def test_breakdown_numeric_hogql(self): [ (BREAKDOWN_NULL_NUMERIC_LABEL, 6.0, [1.0, 0.0, 1.0, 4.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), (19, 2.0, [2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), - (BREAKDOWN_OTHER_NUMERIC_LABEL, 1.0, [1.0]), + (BREAKDOWN_OTHER_NUMERIC_LABEL, 1.0, [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), ], ) @@ -559,7 +559,7 @@ def test_breakdown_string_hogql(self): [ (BREAKDOWN_NULL_STRING_LABEL, 6.0, [1.0, 0.0, 1.0, 4.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), ("https://example.com", 2.0, [2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), - (BREAKDOWN_OTHER_STRING_LABEL, 1.0, [1.0]), + (BREAKDOWN_OTHER_STRING_LABEL, 1.0, [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), ], ) diff --git a/posthog/schema.py b/posthog/schema.py index 4bdd25e88378d..98710a4c88369 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -351,7 +351,7 @@ class NodeKind(str, Enum): PersonsNode = "PersonsNode" HogQLQuery = "HogQLQuery" HogQLMetadata = "HogQLMetadata" - PersonsQuery = "PersonsQuery" + ActorsQuery = "ActorsQuery" SessionsTimelineQuery = "SessionsTimelineQuery" DataTableNode = "DataTableNode" DataVisualizationNode = "DataVisualizationNode" @@ -363,7 +363,7 @@ class NodeKind(str, Enum): PathsQuery = "PathsQuery" StickinessQuery = "StickinessQuery" LifecycleQuery = "LifecycleQuery" - InsightPersonsQuery = "InsightPersonsQuery" + InsightActorsQuery = "InsightActorsQuery" WebOverviewQuery = "WebOverviewQuery" WebTopClicksQuery = "WebTopClicksQuery" WebStatsTableQuery = "WebStatsTableQuery" @@ -733,6 +733,21 @@ class WebTopClicksQueryResponse(BaseModel): types: Optional[List] = None +class ActorsQueryResponse(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + columns: List + hasMore: Optional[bool] = None + hogql: str + limit: int + missing_actors_count: Optional[int] = None + offset: int + results: List[List] + timings: Optional[List[QueryTiming]] = None + types: List[str] + + class AnyResponseTypeItem(BaseModel): model_config = ConfigDict( extra="forbid", @@ -810,8 +825,8 @@ class EventsQueryResponse(BaseModel): columns: List hasMore: Optional[bool] = None hogql: str - limit: int - offset: int + limit: Optional[int] = None + offset: Optional[int] = None results: List[List] timings: Optional[List[QueryTiming]] = None types: List[str] @@ -943,21 +958,6 @@ class PersonPropertyFilter(BaseModel): value: Optional[Union[str, float, List[Union[str, float]]]] = None -class PersonsQueryResponse(BaseModel): - model_config = ConfigDict( - extra="forbid", - ) - columns: List - hasMore: Optional[bool] = None - hogql: str - limit: int - missing_actors_count: Optional[int] = None - offset: int - results: List[List] - timings: Optional[List[QueryTiming]] = None - types: List[str] - - class QueryResponse(BaseModel): model_config = ConfigDict( extra="forbid", @@ -1877,7 +1877,7 @@ class InsightVizNode(BaseModel): vizSpecificOptions: Optional[VizSpecificOptions] = None -class InsightPersonsQuery(BaseModel): +class InsightActorsQuery(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -1885,13 +1885,13 @@ class InsightPersonsQuery(BaseModel): interval: Optional[int] = Field( default=None, description="An interval selected out of available intervals in source query" ) - kind: Literal["InsightPersonsQuery"] = "InsightPersonsQuery" - response: Optional[PersonsQueryResponse] = None + kind: Literal["InsightActorsQuery"] = "InsightActorsQuery" + response: Optional[ActorsQueryResponse] = None source: Union[TrendsQuery, FunnelsQuery, RetentionQuery, PathsQuery, StickinessQuery, LifecycleQuery] status: Optional[str] = None -class PersonsQuery(BaseModel): +class ActorsQuery(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -1911,7 +1911,7 @@ class PersonsQuery(BaseModel): ] ] ] = None - kind: Literal["PersonsQuery"] = "PersonsQuery" + kind: Literal["ActorsQuery"] = "ActorsQuery" limit: Optional[int] = None offset: Optional[int] = None orderBy: Optional[List[str]] = None @@ -1931,10 +1931,10 @@ class PersonsQuery(BaseModel): ] ] ] = None - response: Optional[PersonsQueryResponse] = Field(default=None, description="Cached query response") + response: Optional[ActorsQueryResponse] = Field(default=None, description="Cached query response") search: Optional[str] = None select: Optional[List[str]] = None - source: Optional[Union[InsightPersonsQuery, HogQLQuery]] = None + source: Optional[Union[InsightActorsQuery, HogQLQuery]] = None class DataTableNode(BaseModel): @@ -1984,7 +1984,7 @@ class DataTableNode(BaseModel): EventsNode, EventsQuery, PersonsNode, - PersonsQuery, + ActorsQuery, HogQLQuery, TimeToSeeDataSessionsQuery, WebOverviewQuery, @@ -2013,8 +2013,8 @@ class QuerySchema(RootModel): PersonsNode, TimeToSeeDataSessionsQuery, EventsQuery, - PersonsQuery, - InsightPersonsQuery, + ActorsQuery, + InsightActorsQuery, SessionsTimelineQuery, HogQLQuery, HogQLMetadata, diff --git a/posthog/session_recordings/queries/session_recording_list_from_replay_summary.py b/posthog/session_recordings/queries/session_recording_list_from_replay_summary.py index f4b6135805845..57c7a8864193f 100644 --- a/posthog/session_recordings/queries/session_recording_list_from_replay_summary.py +++ b/posthog/session_recordings/queries/session_recording_list_from_replay_summary.py @@ -151,7 +151,7 @@ def get_query(self) -> Tuple[str, Dict]: } -class PersonsQuery(EventQuery): +class ActorsQuery(EventQuery): _filter: SessionRecordingsFilter # we have to implement this from EventQuery but don't need it @@ -459,7 +459,7 @@ def get_query(self, select_event_ids: bool = False) -> Tuple[str, Dict[str, Any] ) def _persons_join_or_subquery(self, event_filters, prop_query): - persons_select, persons_select_params = PersonsQuery(filter=self._filter, team=self._team).get_query() + persons_select, persons_select_params = ActorsQuery(filter=self._filter, team=self._team).get_query() persons_join = "" persons_sub_query = "" if persons_select: @@ -632,7 +632,7 @@ def get_query(self) -> Tuple[str, Dict[str, Any]]: if events_select: events_select = f"AND s.session_id in (select `$session_id` as session_id from ({events_select}) as session_events_sub_query)" - persons_select, persons_select_params = PersonsQuery(filter=self._filter, team=self._team).get_query() + persons_select, persons_select_params = ActorsQuery(filter=self._filter, team=self._team).get_query() if persons_select: persons_select = ( f"AND s.distinct_id in (select distinct_id from ({persons_select}) as session_persons_sub_query)" diff --git a/posthog/tasks/__init__.py b/posthog/tasks/__init__.py index 261a4c33ef1a5..80d3661259f16 100644 --- a/posthog/tasks/__init__.py +++ b/posthog/tasks/__init__.py @@ -7,6 +7,7 @@ demo_create_data, email, exporter, + process_scheduled_changes, split_person, sync_all_organization_available_features, usage_report, @@ -20,6 +21,7 @@ "demo_create_data", "email", "exporter", + "process_scheduled_changes", "split_person", "sync_all_organization_available_features", "user_identify", diff --git a/posthog/tasks/process_scheduled_changes.py b/posthog/tasks/process_scheduled_changes.py new file mode 100644 index 0000000000000..22d09e9948d35 --- /dev/null +++ b/posthog/tasks/process_scheduled_changes.py @@ -0,0 +1,39 @@ +from posthog.models import ScheduledChange +from django.utils import timezone +from posthog.models import FeatureFlag +from django.db import transaction, OperationalError + +models = {"FeatureFlag": FeatureFlag} + + +def process_scheduled_changes() -> None: + try: + with transaction.atomic(): + scheduled_changes = ( + ScheduledChange.objects.select_for_update(nowait=True) + .filter( + executed_at__isnull=True, + scheduled_at__lte=timezone.now(), + ) + .order_by("scheduled_at")[:10000] + ) + + for scheduled_change in scheduled_changes: + try: + # Execute the change on the model instance + model = models[scheduled_change.model_name] + instance = model.objects.get(id=scheduled_change.record_id) + instance.scheduled_changes_dispatcher(scheduled_change.payload) + + # Mark scheduled change completed + scheduled_change.executed_at = timezone.now() + scheduled_change.save() + + except Exception as e: + # Store the failure reason + scheduled_change.failure_reason = str(e) + scheduled_change.executed_at = timezone.now() + scheduled_change.save() + except OperationalError: + # Failed to obtain the lock + pass diff --git a/posthog/tasks/test/__snapshots__/test_process_scheduled_changes.ambr b/posthog/tasks/test/__snapshots__/test_process_scheduled_changes.ambr new file mode 100644 index 0000000000000..87019fd274336 --- /dev/null +++ b/posthog/tasks/test/__snapshots__/test_process_scheduled_changes.ambr @@ -0,0 +1,291 @@ +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes + ' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE ("posthog_featureflag"."active" + AND NOT "posthog_featureflag"."deleted" + AND "posthog_featureflag"."team_id" = 2) + ' +--- +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.1 + ' + SELECT "posthog_scheduledchange"."id", + "posthog_scheduledchange"."record_id", + "posthog_scheduledchange"."model_name", + "posthog_scheduledchange"."payload", + "posthog_scheduledchange"."scheduled_at", + "posthog_scheduledchange"."executed_at", + "posthog_scheduledchange"."failure_reason", + "posthog_scheduledchange"."team_id", + "posthog_scheduledchange"."created_at", + "posthog_scheduledchange"."created_by_id", + "posthog_scheduledchange"."updated_at" + FROM "posthog_scheduledchange" + WHERE ("posthog_scheduledchange"."executed_at" IS NULL + AND "posthog_scheduledchange"."scheduled_at" <= '2023-12-21T09:00:00+00:00'::timestamptz) + ORDER BY "posthog_scheduledchange"."scheduled_at" ASC + LIMIT 10000 + FOR + UPDATE NOWAIT + ' +--- +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.10 + ' + SELECT "posthog_scheduledchange"."id", + "posthog_scheduledchange"."record_id", + "posthog_scheduledchange"."model_name", + "posthog_scheduledchange"."payload", + "posthog_scheduledchange"."scheduled_at", + "posthog_scheduledchange"."executed_at", + "posthog_scheduledchange"."failure_reason", + "posthog_scheduledchange"."team_id", + "posthog_scheduledchange"."created_at", + "posthog_scheduledchange"."created_by_id", + "posthog_scheduledchange"."updated_at" + FROM "posthog_scheduledchange" + WHERE "posthog_scheduledchange"."id" = 2 + LIMIT 21 + ' +--- +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.11 + ' + SELECT "posthog_scheduledchange"."id", + "posthog_scheduledchange"."record_id", + "posthog_scheduledchange"."model_name", + "posthog_scheduledchange"."payload", + "posthog_scheduledchange"."scheduled_at", + "posthog_scheduledchange"."executed_at", + "posthog_scheduledchange"."failure_reason", + "posthog_scheduledchange"."team_id", + "posthog_scheduledchange"."created_at", + "posthog_scheduledchange"."created_by_id", + "posthog_scheduledchange"."updated_at" + FROM "posthog_scheduledchange" + WHERE "posthog_scheduledchange"."id" = 2 + LIMIT 21 + ' +--- +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.12 + ' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE "posthog_featureflag"."key" = 'flag-1' + LIMIT 21 + ' +--- +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.2 + ' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE "posthog_featureflag"."id" = 2 + LIMIT 21 + ' +--- +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.3 + ' + SELECT "posthog_user"."id", + "posthog_user"."password", + "posthog_user"."last_login", + "posthog_user"."first_name", + "posthog_user"."last_name", + "posthog_user"."is_staff", + "posthog_user"."is_active", + "posthog_user"."date_joined", + "posthog_user"."uuid", + "posthog_user"."current_organization_id", + "posthog_user"."current_team_id", + "posthog_user"."email", + "posthog_user"."pending_email", + "posthog_user"."temporary_token", + "posthog_user"."distinct_id", + "posthog_user"."is_email_verified", + "posthog_user"."requested_password_reset_at", + "posthog_user"."has_seen_product_intro_for", + "posthog_user"."email_opt_in", + "posthog_user"."theme_mode", + "posthog_user"."partial_notification_settings", + "posthog_user"."anonymize_data", + "posthog_user"."toolbar_mode", + "posthog_user"."events_column_config" + FROM "posthog_user" + WHERE "posthog_user"."id" = 2 + LIMIT 21 + ' +--- +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.4 + ' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE ("posthog_featureflag"."active" + AND NOT "posthog_featureflag"."deleted" + AND "posthog_featureflag"."team_id" = 2) + ' +--- +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.5 + ' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE "posthog_featureflag"."id" = 2 + LIMIT 21 + ' +--- +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.6 + ' + SELECT "posthog_user"."id", + "posthog_user"."password", + "posthog_user"."last_login", + "posthog_user"."first_name", + "posthog_user"."last_name", + "posthog_user"."is_staff", + "posthog_user"."is_active", + "posthog_user"."date_joined", + "posthog_user"."uuid", + "posthog_user"."current_organization_id", + "posthog_user"."current_team_id", + "posthog_user"."email", + "posthog_user"."pending_email", + "posthog_user"."temporary_token", + "posthog_user"."distinct_id", + "posthog_user"."is_email_verified", + "posthog_user"."requested_password_reset_at", + "posthog_user"."has_seen_product_intro_for", + "posthog_user"."email_opt_in", + "posthog_user"."theme_mode", + "posthog_user"."partial_notification_settings", + "posthog_user"."anonymize_data", + "posthog_user"."toolbar_mode", + "posthog_user"."events_column_config" + FROM "posthog_user" + WHERE "posthog_user"."id" = 2 + LIMIT 21 + ' +--- +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.7 + ' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE ("posthog_featureflag"."active" + AND NOT "posthog_featureflag"."deleted" + AND "posthog_featureflag"."team_id" = 2) + ' +--- +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.8 + ' + SELECT "posthog_scheduledchange"."id", + "posthog_scheduledchange"."record_id", + "posthog_scheduledchange"."model_name", + "posthog_scheduledchange"."payload", + "posthog_scheduledchange"."scheduled_at", + "posthog_scheduledchange"."executed_at", + "posthog_scheduledchange"."failure_reason", + "posthog_scheduledchange"."team_id", + "posthog_scheduledchange"."created_at", + "posthog_scheduledchange"."created_by_id", + "posthog_scheduledchange"."updated_at" + FROM "posthog_scheduledchange" + WHERE "posthog_scheduledchange"."id" = 2 + LIMIT 21 + ' +--- +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.9 + ' + SELECT "posthog_scheduledchange"."id", + "posthog_scheduledchange"."record_id", + "posthog_scheduledchange"."model_name", + "posthog_scheduledchange"."payload", + "posthog_scheduledchange"."scheduled_at", + "posthog_scheduledchange"."executed_at", + "posthog_scheduledchange"."failure_reason", + "posthog_scheduledchange"."team_id", + "posthog_scheduledchange"."created_at", + "posthog_scheduledchange"."created_by_id", + "posthog_scheduledchange"."updated_at" + FROM "posthog_scheduledchange" + WHERE "posthog_scheduledchange"."id" = 2 + LIMIT 21 + ' +--- diff --git a/posthog/tasks/test/test_process_scheduled_changes.py b/posthog/tasks/test/test_process_scheduled_changes.py new file mode 100644 index 0000000000000..866f3847c5d34 --- /dev/null +++ b/posthog/tasks/test/test_process_scheduled_changes.py @@ -0,0 +1,179 @@ +from datetime import datetime, timedelta, timezone +from posthog.models import ScheduledChange, FeatureFlag +from posthog.test.base import APIBaseTest, QueryMatchingTest, snapshot_postgres_queries +from posthog.tasks.process_scheduled_changes import process_scheduled_changes +from freezegun import freeze_time + + +class TestProcessScheduledChanges(APIBaseTest, QueryMatchingTest): + def test_schedule_feature_flag_set_active(self) -> None: + feature_flag = FeatureFlag.objects.create( + name="Flag 1", + key="flag-1", + active=False, + filters={"groups": []}, + team=self.team, + created_by=self.user, + ) + + ScheduledChange.objects.create( + team=self.team, + record_id=feature_flag.id, + model_name="FeatureFlag", + payload={"operation": "update_status", "value": True}, + scheduled_at=(datetime.now(timezone.utc) - timedelta(seconds=30)).isoformat(), + ) + + process_scheduled_changes() + + updated_flag = FeatureFlag.objects.get(key="flag-1") + self.assertEqual(updated_flag.active, True) + + def test_schedule_feature_flag_add_release_condition(self) -> None: + feature_flag = FeatureFlag.objects.create( + name="Flag 1", + key="flag-1", + active=False, + filters={"groups": []}, + team=self.team, + created_by=self.user, + ) + + new_release_condition = { + "variant": None, + "properties": [{"key": "$browser", "type": "person", "value": ["Chrome"], "operator": "exact"}], + "rollout_percentage": 30, + } + + payload = { + "operation": "add_release_condition", + "value": {"groups": [new_release_condition], "payloads": {}, "multivariate": None}, + } + + ScheduledChange.objects.create( + team=self.team, + record_id=feature_flag.id, + model_name="FeatureFlag", + payload=payload, + scheduled_at=(datetime.now(timezone.utc) - timedelta(seconds=30)), + ) + + process_scheduled_changes() + + updated_flag = FeatureFlag.objects.get(key="flag-1") + self.assertEqual(updated_flag.filters["groups"][0], new_release_condition) + + def test_schedule_feature_flag_invalid_payload(self) -> None: + feature_flag = FeatureFlag.objects.create( + name="Flag 1", + key="flag-1", + active=False, + filters={"groups": []}, + team=self.team, + created_by=self.user, + ) + + payload = {"foo": "bar"} + + scheduled_change = ScheduledChange.objects.create( + team=self.team, + record_id=feature_flag.id, + model_name="FeatureFlag", + payload=payload, + scheduled_at=(datetime.now(timezone.utc) - timedelta(seconds=30)), + ) + + process_scheduled_changes() + + updated_flag = FeatureFlag.objects.get(key="flag-1") + self.assertEqual(updated_flag.filters["groups"], []) + + updated_scheduled_change = ScheduledChange.objects.get(id=scheduled_change.id) + self.assertEqual(updated_scheduled_change.failure_reason, "Invalid payload") + + @snapshot_postgres_queries + @freeze_time("2023-12-21T09:00:00Z") + def test_schedule_feature_flag_multiple_changes(self) -> None: + feature_flag = FeatureFlag.objects.create( + name="Flag", + key="flag-1", + active=True, + filters={"groups": []}, + team=self.team, + created_by=self.user, + ) + + # Create 4 scheduled changes + # 1. Due in the past + change_past_condition = { + "properties": [{"key": "$geoip_city_name", "value": ["Sydney"], "operator": "exact", "type": "person"}], + "rollout_percentage": 50, + "variant": None, + } + change_past = ScheduledChange.objects.create( + team=self.team, + record_id=feature_flag.id, + model_name="FeatureFlag", + payload={ + "operation": "add_release_condition", + "value": {"groups": [change_past_condition], "multivariate": None, "payloads": {}}, + }, + scheduled_at=(datetime.now(timezone.utc) - timedelta(hours=1)), + ) + + # 2. Due in the past and already executed + change_past_executed_at = datetime.now(timezone.utc) - timedelta(hours=5) + change_past_executed = ScheduledChange.objects.create( + team=self.team, + record_id=feature_flag.id, + model_name="FeatureFlag", + payload={"operation": "update_status", "value": False}, + scheduled_at=change_past_executed_at, + executed_at=change_past_executed_at, + ) + + # 3. Due exactly now + change_due_now_condition = { + "properties": [{"key": "$geoip_city_name", "value": ["New York"], "operator": "exact", "type": "person"}], + "rollout_percentage": 75, + "variant": None, + } + change_due_now = ScheduledChange.objects.create( + team=self.team, + record_id=feature_flag.id, + model_name="FeatureFlag", + payload={ + "operation": "add_release_condition", + "value": {"groups": [change_due_now_condition], "multivariate": None, "payloads": {}}, + }, + scheduled_at=datetime.now(timezone.utc), + ) + + # 4. Due in the future + change_due_future = ScheduledChange.objects.create( + team=self.team, + record_id=feature_flag.id, + model_name="FeatureFlag", + payload={"operation": "update_status", "value": False}, + scheduled_at=(datetime.now(timezone.utc) + timedelta(hours=1)), + ) + + process_scheduled_changes() + + # Refresh change records + change_past = ScheduledChange.objects.get(id=change_past.id) + change_past_executed = ScheduledChange.objects.get(id=change_past_executed.id) + change_due_now = ScheduledChange.objects.get(id=change_due_now.id) + change_due_future = ScheduledChange.objects.get(id=change_due_future.id) + + # Changes due have been marked executed + self.assertIsNotNone(change_past.executed_at) + self.assertIsNotNone(change_due_now.executed_at) + + # Other changes have not been executed + self.assertEqual(change_past_executed.executed_at, change_past_executed_at) + self.assertIsNone(change_due_future.executed_at) + + # The changes due have been propagated in the correct order (oldest scheduled_at first) + updated_flag = FeatureFlag.objects.get(key="flag-1") + self.assertEqual(updated_flag.filters["groups"], [change_past_condition, change_due_now_condition])