From d66eaa12f2e17a326fb35ea7c82ee065d5b2bbd0 Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Mon, 25 Mar 2024 20:08:41 +0100 Subject: [PATCH 1/6] fix(insights): Attribute async queries to users (#21019) * fix(insights): Attribute async queries to users * Fix re-raising ClickHouse errors from threads * Improve typing * Update query snapshots * Fix more typing * Update query snapshots * Update query snapshots * Update schema.py --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- posthog/api/query.py | 3 +- posthog/clickhouse/client/execute_async.py | 44 ++++++++----- .../client/test/test_execute_async.py | 62 +++++++++++++------ posthog/errors.py | 7 ++- posthog/tasks/tasks.py | 11 +++- 5 files changed, 86 insertions(+), 41 deletions(-) diff --git a/posthog/api/query.py b/posthog/api/query.py index d8f45531253a0..e30853655c749 100644 --- a/posthog/api/query.py +++ b/posthog/api/query.py @@ -67,9 +67,10 @@ def create(self, request, *args, **kwargs) -> Response: if data.async_: query_status = enqueue_process_query_task( team_id=self.team.pk, + user_id=self.request.user.pk, query_json=request.data["query"], query_id=client_query_id, - refresh_requested=data.refresh, + refresh_requested=data.refresh or False, ) return Response(query_status.model_dump(), status=status.HTTP_202_ACCEPTED) diff --git a/posthog/clickhouse/client/execute_async.py b/posthog/clickhouse/client/execute_async.py index 06f7fc639f824..4671b0060299b 100644 --- a/posthog/clickhouse/client/execute_async.py +++ b/posthog/clickhouse/client/execute_async.py @@ -1,5 +1,6 @@ import datetime import json +from typing import Optional import uuid import structlog @@ -69,11 +70,12 @@ def delete_query_status(self): def execute_process_query( - team_id, - query_id, - query_json, - limit_context, - refresh_requested, + team_id: int, + user_id: int, + query_id: str, + query_json: dict, + limit_context: Optional[LimitContext], + refresh_requested: bool, ): manager = QueryStatusManager(query_id, team_id) @@ -91,7 +93,7 @@ def execute_process_query( QUERY_WAIT_TIME.observe(wait_duration) try: - tag_queries(client_query_id=query_id, team_id=team_id) + tag_queries(client_query_id=query_id, team_id=team_id, user_id=user_id) results = process_query( team=team, query_json=query_json, limit_context=limit_context, refresh_requested=refresh_requested ) @@ -113,12 +115,13 @@ def execute_process_query( def enqueue_process_query_task( - team_id, - query_json, - query_id=None, - refresh_requested=False, - bypass_celery=False, - force=False, + team_id: int, + user_id: int, + query_json: dict, + query_id: Optional[str] = None, + refresh_requested: bool = False, + force: bool = False, + _test_only_bypass_celery: bool = False, ) -> QueryStatus: if not query_id: query_id = uuid.uuid4().hex @@ -136,14 +139,23 @@ def enqueue_process_query_task( query_status = QueryStatus(id=query_id, team_id=team_id, start_time=datetime.datetime.now(datetime.timezone.utc)) manager.store_query_status(query_status) - if bypass_celery: - # Call directly ( for testing ) + if _test_only_bypass_celery: process_query_task( - team_id, query_id, query_json, limit_context=LimitContext.QUERY_ASYNC, refresh_requested=refresh_requested + team_id, + user_id, + query_id, + query_json, + limit_context=LimitContext.QUERY_ASYNC, + refresh_requested=refresh_requested, ) else: task = process_query_task.delay( - team_id, query_id, query_json, limit_context=LimitContext.QUERY_ASYNC, refresh_requested=refresh_requested + team_id, + user_id, + query_id, + query_json, + limit_context=LimitContext.QUERY_ASYNC, + refresh_requested=refresh_requested, ) query_status.task_id = task.id manager.store_query_status(query_status) diff --git a/posthog/clickhouse/client/test/test_execute_async.py b/posthog/clickhouse/client/test/test_execute_async.py index 0d7a7281e6a4b..085e7708b9232 100644 --- a/posthog/clickhouse/client/test/test_execute_async.py +++ b/posthog/clickhouse/client/test/test_execute_async.py @@ -24,6 +24,7 @@ def setUp(self): self.organization = Organization.objects.create(name="test") self.team = Team.objects.create(organization=self.organization) self.team_id = self.team.pk + self.user_id = 1337 self.query_id = "test_query_id" self.query_json = {} self.limit_context = None @@ -41,7 +42,9 @@ def test_execute_process_query(self, mock_process_query, mock_redis_client): mock_process_query.return_value = [float("inf"), float("-inf"), float("nan"), 1.0, "👍"] - execute_process_query(self.team_id, self.query_id, self.query_json, self.limit_context, self.refresh_requested) + execute_process_query( + self.team_id, self.user_id, self.query_id, self.query_json, self.limit_context, self.refresh_requested + ) mock_redis_client.assert_called_once() mock_process_query.assert_called_once() @@ -55,15 +58,16 @@ def test_execute_process_query(self, mock_process_query, mock_redis_client): class ClickhouseClientTestCase(TestCase, ClickhouseTestMixin): def setUp(self): - self.organization = Organization.objects.create(name="test") - self.team = Team.objects.create(organization=self.organization) - self.team_id = self.team.pk + self.organization: Organization = Organization.objects.create(name="test") + self.team: Team = Team.objects.create(organization=self.organization) + self.team_id: int = self.team.pk + self.user_id: int = 2137 @snapshot_clickhouse_queries def test_async_query_client(self): query = build_query("SELECT 1+1") team_id = self.team_id - query_id = client.enqueue_process_query_task(team_id, query, bypass_celery=True).id + query_id = client.enqueue_process_query_task(team_id, self.user_id, query, _test_only_bypass_celery=True).id result = client.get_query_status(team_id, query_id) self.assertFalse(result.error, result.error_message) self.assertTrue(result.complete) @@ -74,11 +78,13 @@ def test_async_query_client_errors(self): self.assertRaises( HogQLException, client.enqueue_process_query_task, - **{"team_id": (self.team_id), "query_json": query, "bypass_celery": True}, + **{"team_id": self.team_id, "user_id": self.user_id, "query_json": query, "_test_only_bypass_celery": True}, ) query_id = uuid.uuid4().hex try: - client.enqueue_process_query_task(self.team_id, query, query_id=query_id, bypass_celery=True) + client.enqueue_process_query_task( + self.team_id, self.user_id, query, query_id=query_id, _test_only_bypass_celery=True + ) except Exception: pass @@ -89,7 +95,7 @@ def test_async_query_client_errors(self): def test_async_query_client_uuid(self): query = build_query("SELECT toUUID('00000000-0000-0000-0000-000000000000')") team_id = self.team_id - query_id = client.enqueue_process_query_task(team_id, query, bypass_celery=True).id + query_id = client.enqueue_process_query_task(team_id, self.user_id, query, _test_only_bypass_celery=True).id result = client.get_query_status(team_id, query_id) self.assertFalse(result.error, result.error_message) self.assertTrue(result.complete) @@ -99,7 +105,7 @@ def test_async_query_client_does_not_leak(self): query = build_query("SELECT 1+1") team_id = self.team_id wrong_team = 5 - query_id = client.enqueue_process_query_task(team_id, query, bypass_celery=True).id + query_id = client.enqueue_process_query_task(team_id, self.user_id, query, _test_only_bypass_celery=True).id try: client.get_query_status(wrong_team, query_id) @@ -111,13 +117,19 @@ def test_async_query_client_is_lazy(self, execute_sync_mock): query = build_query("SELECT 4 + 4") query_id = uuid.uuid4().hex team_id = self.team_id - client.enqueue_process_query_task(team_id, query, query_id=query_id, bypass_celery=True) + client.enqueue_process_query_task( + team_id, self.user_id, query, query_id=query_id, _test_only_bypass_celery=True + ) # Try the same query again - client.enqueue_process_query_task(team_id, query, query_id=query_id, bypass_celery=True) + client.enqueue_process_query_task( + team_id, self.user_id, query, query_id=query_id, _test_only_bypass_celery=True + ) # Try the same query again (for good measure!) - client.enqueue_process_query_task(team_id, query, query_id=query_id, bypass_celery=True) + client.enqueue_process_query_task( + team_id, self.user_id, query, query_id=query_id, _test_only_bypass_celery=True + ) # Assert that we only called clickhouse once execute_sync_mock.assert_called_once() @@ -127,13 +139,19 @@ def test_async_query_client_is_lazy_but_not_too_lazy(self, execute_sync_mock): query = build_query("SELECT 8 + 8") query_id = uuid.uuid4().hex team_id = self.team_id - client.enqueue_process_query_task(team_id, query, query_id=query_id, bypass_celery=True) + client.enqueue_process_query_task( + team_id, self.user_id, query, query_id=query_id, _test_only_bypass_celery=True + ) # Try the same query again, but with force - client.enqueue_process_query_task(team_id, query, query_id=query_id, bypass_celery=True, force=True) + client.enqueue_process_query_task( + team_id, self.user_id, query, query_id=query_id, _test_only_bypass_celery=True, force=True + ) # Try the same query again (for good measure!) - client.enqueue_process_query_task(team_id, query, query_id=query_id, bypass_celery=True) + client.enqueue_process_query_task( + team_id, self.user_id, query, query_id=query_id, _test_only_bypass_celery=True + ) # Assert that we called clickhouse twice self.assertEqual(execute_sync_mock.call_count, 2) @@ -145,13 +163,19 @@ def test_async_query_client_manual_query_uuid(self, execute_sync_mock): query = build_query("SELECT 8 + 8") team_id = self.team_id query_id = "I'm so unique" - client.enqueue_process_query_task(team_id, query, query_id=query_id, bypass_celery=True) + client.enqueue_process_query_task( + team_id, self.user_id, query, query_id=query_id, _test_only_bypass_celery=True + ) # Try the same query again, but with force - client.enqueue_process_query_task(team_id, query, query_id=query_id, bypass_celery=True, force=True) + client.enqueue_process_query_task( + team_id, self.user_id, query, query_id=query_id, _test_only_bypass_celery=True, force=True + ) # Try the same query again (for good measure!) - client.enqueue_process_query_task(team_id, query, query_id=query_id, bypass_celery=True) + client.enqueue_process_query_task( + team_id, self.user_id, query, query_id=query_id, _test_only_bypass_celery=True + ) # Assert that we called clickhouse twice self.assertEqual(execute_sync_mock.call_count, 2) @@ -186,4 +210,4 @@ def test_client_strips_comments_from_request(self): # Make sure it still includes the "annotation" comment that includes # request routing information for debugging purposes - self.assertIn("/* request:1 */", first_query) + self.assertIn(f"/* user_id:{self.user_id} request:1 */", first_query) diff --git a/posthog/errors.py b/posthog/errors.py index afa0cdd8648e7..a6e3536042a7f 100644 --- a/posthog/errors.py +++ b/posthog/errors.py @@ -1,6 +1,6 @@ from dataclasses import dataclass import re -from typing import Dict +from typing import Dict, Optional from clickhouse_driver.errors import ServerException @@ -8,9 +8,10 @@ class InternalCHQueryError(ServerException): - code_name: str + code_name: Optional[str] + """Can be null if re-raised from a thread (see `failhard_threadhook_context`).""" - def __init__(self, message, *, code=None, nested=None, code_name): + def __init__(self, message, *, code=None, nested=None, code_name=None): self.code_name = code_name super().__init__(message, code, nested) diff --git a/posthog/tasks/tasks.py b/posthog/tasks/tasks.py index 5eff6afd33fe2..bead27cbd1eec 100644 --- a/posthog/tasks/tasks.py +++ b/posthog/tasks/tasks.py @@ -1,5 +1,5 @@ import time -from typing import Any, Optional +from typing import Optional from uuid import UUID from celery import shared_task @@ -9,6 +9,7 @@ from prometheus_client import Gauge from posthog.cloud_utils import is_cloud +from posthog.hogql.constants import LimitContext from posthog.metrics import pushed_metrics_registry from posthog.ph_client import get_ph_client from posthog.redis import get_client @@ -33,7 +34,12 @@ def redis_heartbeat() -> None: @shared_task(ignore_result=True, queue=CeleryQueue.ANALYTICS_QUERIES.value) def process_query_task( - team_id: str, query_id: str, query_json: Any, limit_context: Any = None, refresh_requested: bool = False + team_id: int, + user_id: int, + query_id: str, + query_json: dict, + limit_context: Optional[LimitContext] = None, + refresh_requested: bool = False, ) -> None: """ Kick off query @@ -43,6 +49,7 @@ def process_query_task( execute_process_query( team_id=team_id, + user_id=user_id, query_id=query_id, query_json=query_json, limit_context=limit_context, From d226bd034a0c3238b47a4cda05cd5c84506700e1 Mon Sep 17 00:00:00 2001 From: Zach Waterfield Date: Mon, 25 Mar 2024 14:01:24 -0600 Subject: [PATCH 2/6] feat: add a/b test for customer logos on signup page (#21086) * Add customer logos * Update the signup left panel to have a third a/b test w/ customer logos * Update a/b testing key for signup language * Update logos text to use control * Update UI snapshots for `chromium` (1) * Update constants.tsx * Update SignupContainer.tsx * Update frontend/src/scenes/authentication/signup/SignupContainer.tsx Co-authored-by: Bianca Yang <21014901+xrdt@users.noreply.github.com> * Update frontend/src/scenes/authentication/signup/SignupContainer.tsx * Remove text class name * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (2) --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Bianca Yang <21014901+xrdt@users.noreply.github.com> --- frontend/src/lib/components/CustomerLogo.tsx | 23 ++++ frontend/src/lib/constants.tsx | 2 +- frontend/src/lib/customers/airbus.svg | 1 + frontend/src/lib/customers/hasura.svg | 1 + frontend/src/lib/customers/staples.svg | 1 + frontend/src/lib/customers/y-combinator.svg | 1 + .../authentication/signup/SignupContainer.tsx | 111 ++++++++++++------ 7 files changed, 102 insertions(+), 38 deletions(-) create mode 100644 frontend/src/lib/components/CustomerLogo.tsx create mode 100644 frontend/src/lib/customers/airbus.svg create mode 100644 frontend/src/lib/customers/hasura.svg create mode 100644 frontend/src/lib/customers/staples.svg create mode 100644 frontend/src/lib/customers/y-combinator.svg diff --git a/frontend/src/lib/components/CustomerLogo.tsx b/frontend/src/lib/components/CustomerLogo.tsx new file mode 100644 index 0000000000000..659f739d1d7dc --- /dev/null +++ b/frontend/src/lib/components/CustomerLogo.tsx @@ -0,0 +1,23 @@ +interface CustomerProps { + image: string + alt: string + className?: string +} + +interface LogoProps { + src: string + alt: string + className?: string +} + +const Logo = ({ src, alt, className = '' }: LogoProps): JSX.Element => ( + {alt} +) + +export const CustomerLogo = ({ image, alt, className = '' }: CustomerProps): JSX.Element => { + return ( +
  • + +
  • + ) +} diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index f9d7a552e6be7..c2d40d03957fe 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -149,7 +149,7 @@ export const FEATURE_FLAGS = { POSTHOG_3000_NAV: 'posthog-3000-nav', // owner: @Twixes HEDGEHOG_MODE: 'hedgehog-mode', // owner: @benjackwhite HEDGEHOG_MODE_DEBUG: 'hedgehog-mode-debug', // owner: @benjackwhite - GENERIC_SIGNUP_BENEFITS: 'generic-signup-benefits', // experiment, owner: @raquelmsmith + SIGNUP_BENEFITS: 'signup-benefits', // experiment, owner: @zlwaterfield WEB_ANALYTICS: 'web-analytics', // owner @robbie-c #team-web-analytics WEB_ANALYTICS_SAMPLING: 'web-analytics-sampling', // owner @robbie-c #team-web-analytics HIGH_FREQUENCY_BATCH_EXPORTS: 'high-frequency-batch-exports', // owner: @tomasfarias diff --git a/frontend/src/lib/customers/airbus.svg b/frontend/src/lib/customers/airbus.svg new file mode 100644 index 0000000000000..ff18cae1c8c0f --- /dev/null +++ b/frontend/src/lib/customers/airbus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/lib/customers/hasura.svg b/frontend/src/lib/customers/hasura.svg new file mode 100644 index 0000000000000..1eb0373ecf1f4 --- /dev/null +++ b/frontend/src/lib/customers/hasura.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/lib/customers/staples.svg b/frontend/src/lib/customers/staples.svg new file mode 100644 index 0000000000000..0e1ff76715798 --- /dev/null +++ b/frontend/src/lib/customers/staples.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/lib/customers/y-combinator.svg b/frontend/src/lib/customers/y-combinator.svg new file mode 100644 index 0000000000000..1d19c5ff15d4a --- /dev/null +++ b/frontend/src/lib/customers/y-combinator.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/scenes/authentication/signup/SignupContainer.tsx b/frontend/src/scenes/authentication/signup/SignupContainer.tsx index 3113cde8b3702..0544f035d60a1 100644 --- a/frontend/src/scenes/authentication/signup/SignupContainer.tsx +++ b/frontend/src/scenes/authentication/signup/SignupContainer.tsx @@ -2,15 +2,21 @@ import { IconCheckCircle } from '@posthog/icons' import { useValues } from 'kea' import { router } from 'kea-router' import { BridgePage } from 'lib/components/BridgePage/BridgePage' +import { CustomerLogo } from 'lib/components/CustomerLogo' import { CLOUD_HOSTNAMES, FEATURE_FLAGS } from 'lib/constants' import { Link } from 'lib/lemon-ui/Link' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { featureFlagLogic, FeatureFlagsSet } from 'lib/logic/featureFlagLogic' +import { ReactNode } from 'react' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { SceneExport } from 'scenes/sceneTypes' import { userLogic } from 'scenes/userLogic' import { Region } from '~/types' +import airbus from '../../../lib/customers/airbus.svg' +import hasura from '../../../lib/customers/hasura.svg' +import staples from '../../../lib/customers/staples.svg' +import yCombinator from '../../../lib/customers/y-combinator.svg' import { SignupForm } from './signupForm/SignupForm' export const scene: SceneExport = { @@ -46,51 +52,82 @@ export function SignupContainer(): JSX.Element | null { ) : null } +type ProductBenefit = { + benefit: string + description: string | ReactNode +} + +const getProductBenefits = (featureFlags: FeatureFlagsSet): ProductBenefit[] => { + const signupBenefitsFlag = featureFlags[FEATURE_FLAGS.SIGNUP_BENEFITS] + switch (signupBenefitsFlag) { + case 'generic-language': + return [ + { + benefit: 'Free usage every month - even on paid plans', + description: '1M free events, 5K free session recordings, and more. Every month. Forever.', + }, + { + benefit: 'Start collecting data immediately', + description: 'Integrate with developer-friendly APIs or a low-code web snippet.', + }, + { + benefit: 'Join industry leaders that run on PostHog', + description: + 'Airbus, Hasura, Y Combinator, Staples, and thousands more trust PostHog as their Product OS.', + }, + ] + case 'logos': + return [ + { + benefit: '1M events free every month', + description: 'Product analytics, feature flags, experiments, and more.', + }, + { + benefit: 'Start collecting events immediately', + description: 'Integrate with developer-friendly APIs or use our easy autocapture script.', + }, + { + benefit: 'Join industry leaders that run on PostHog', + description: ( +
    + {[airbus, hasura, yCombinator, staples].map((company, i) => ( + + + + ))} +
    + ), + }, + ] + default: + return [ + { + benefit: 'Free for 1M events every month', + description: 'Product analytics, feature flags, experiments, and more.', + }, + { + benefit: 'Start collecting events immediately', + description: 'Integrate with developer-friendly APIs or use our easy autocapture script.', + }, + { + benefit: 'Join industry leaders that run on PostHog', + description: + 'Airbus, Hasura, Y Combinator, Staples, and thousands more trust PostHog as their Product OS.', + }, + ] + } +} + export function SignupLeftContainer(): JSX.Element { const { preflight } = useValues(preflightLogic) const { featureFlags } = useValues(featureFlagLogic) - const showGenericSignupBenefits: boolean = featureFlags[FEATURE_FLAGS.GENERIC_SIGNUP_BENEFITS] === 'test' - const getRegionUrl = (region: string): string => { const { pathname, search, hash } = router.values.currentLocation return `https://${CLOUD_HOSTNAMES[region]}${pathname}${search}${hash}` } - const productBenefits: { - benefit: string - description: string - }[] = showGenericSignupBenefits - ? [ - { - benefit: 'Free usage every month - even on paid plans', - description: '1M free events, 5K free session recordings, and more. Every month. Forever.', - }, - { - benefit: 'Start collecting data immediately', - description: 'Integrate with developer-friendly APIs or low-code web snippet.', - }, - { - benefit: 'Join industry leaders that run on PostHog', - description: - 'ClickHouse, Airbus, Hasura, Y Combinator, and thousands more trust PostHog as their Product OS.', - }, - ] - : [ - { - benefit: 'Free for 1M events every month', - description: 'Product analytics, feature flags, experiments, and more.', - }, - { - benefit: 'Start collecting events immediately', - description: 'Integrate with developer-friendly APIs or use our easy autocapture script.', - }, - { - benefit: 'Join industry leaders that run on PostHog', - description: - 'ClickHouse, Airbus, Hasura, Y Combinator, and thousands more trust PostHog as their Product OS.', - }, - ] + const productBenefits = getProductBenefits(featureFlags) return ( <> From 652b75d72f0cd34368e678c801aa7c98dc4f4c5e Mon Sep 17 00:00:00 2001 From: Robbie Date: Mon, 25 Mar 2024 21:14:51 +0000 Subject: [PATCH 3/6] feat(web-analytics): Support null as string in channel type (#21136) Support null as string in channel type --- posthog/hogql/database/schema/channel_type.py | 5 ++++- .../database/schema/test/test_channel_type.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/posthog/hogql/database/schema/channel_type.py b/posthog/hogql/database/schema/channel_type.py index 4954cc5be2b29..1552a0e6aa6d4 100644 --- a/posthog/hogql/database/schema/channel_type.py +++ b/posthog/hogql/database/schema/channel_type.py @@ -63,7 +63,10 @@ def create_channel_type_expr( gad_source: ast.Expr, ) -> ast.Expr: def wrap_with_null_if_empty(expr: ast.Expr) -> ast.Expr: - return ast.Call(name="nullIf", args=[expr, ast.Constant(value="")]) + return ast.Call( + name="nullIf", + args=[ast.Call(name="nullIf", args=[expr, ast.Constant(value="")]), ast.Constant(value="null")], + ) return parse_expr( """ diff --git a/posthog/hogql/database/schema/test/test_channel_type.py b/posthog/hogql/database/schema/test/test_channel_type.py index 10cd4ea4ae009..97dba3e13ba38 100644 --- a/posthog/hogql/database/schema/test/test_channel_type.py +++ b/posthog/hogql/database/schema/test/test_channel_type.py @@ -121,6 +121,21 @@ def test_direct_empty_string(self): ), ) + def test_direct_null_string(self): + self.assertEqual( + "Direct", + self._get_initial_channel_type( + { + "$initial_referring_domain": "$direct", + "$initial_utm_source": "null", + "$initial_utm_medium": "null", + "$initial_utm_campaign": "null", + "$initial_gclid": "null", + "$initial_gad_source": "null", + } + ), + ) + def test_cross_network(self): self.assertEqual( "Cross Network", From fad9f5eb1753e0fcb199324e7b20685882cd81f8 Mon Sep 17 00:00:00 2001 From: Zach Waterfield Date: Mon, 25 Mar 2024 16:05:33 -0600 Subject: [PATCH 4/6] feat: add reverse proxy checker (#21030) * Add reverse proper checker * Create reverseProxyCheckerLogic.test.ts * Update reverseProxyCheckerLogic.test.ts * Update UI snapshots for `chromium` (1) * Update UI snapshots for `chromium` (1) * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (2) * Hook up reverseProxyCheckerLogic to the quick start menu task * Add a timestamp so it doesn't request too often --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .../panels/activation/activationLogic.ts | 9 ++- .../reverseProxyCheckerLogic.test.ts | 63 +++++++++++++++++++ .../reverseProxyCheckerLogic.ts | 49 +++++++++++++++ .../VersionChecker/versionCheckerLogic.ts | 2 +- 4 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 frontend/src/lib/components/ReverseProxyChecker/reverseProxyCheckerLogic.test.ts create mode 100644 frontend/src/lib/components/ReverseProxyChecker/reverseProxyCheckerLogic.ts diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/activation/activationLogic.ts b/frontend/src/layout/navigation-3000/sidepanel/panels/activation/activationLogic.ts index e8afd00aea1ed..b0be99d68f438 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/activation/activationLogic.ts +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/activation/activationLogic.ts @@ -2,6 +2,7 @@ import { actions, connect, events, kea, listeners, path, reducers, selectors } f import { loaders } from 'kea-loaders' import { router } from 'kea-router' import api from 'lib/api' +import { reverseProxyCheckerLogic } from 'lib/components/ReverseProxyChecker/reverseProxyCheckerLogic' import { permanentlyMount } from 'lib/utils/kea-logic-builders' import posthog from 'posthog-js' import { membersLogic } from 'scenes/organization/membersLogic' @@ -58,6 +59,8 @@ export const activationLogic = kea([ ['insights'], dashboardsModel, ['rawDashboards'], + reverseProxyCheckerLogic, + ['hasReverseProxy'], ], actions: [ inviteLogic, @@ -193,6 +196,7 @@ export const activationLogic = kea([ s.customEventsCount, s.installedPlugins, s.currentTeamSkippedTasks, + s.hasReverseProxy, ], ( currentTeam, @@ -202,7 +206,8 @@ export const activationLogic = kea([ dashboards, customEventsCount, installedPlugins, - skippedTasks + skippedTasks, + hasReverseProxy ) => { const tasks: ActivationTaskType[] = [] for (const task of Object.values(ActivationTasks)) { @@ -286,7 +291,7 @@ export const activationLogic = kea([ id: ActivationTasks.SetUpReverseProxy, name: 'Set up a reverse proxy', description: 'Send your events from your own domain to avoid tracking blockers', - completed: false, + completed: hasReverseProxy || false, canSkip: true, skipped: skippedTasks.includes(ActivationTasks.SetUpReverseProxy), url: 'https://posthog.com/docs/advanced/proxy', diff --git a/frontend/src/lib/components/ReverseProxyChecker/reverseProxyCheckerLogic.test.ts b/frontend/src/lib/components/ReverseProxyChecker/reverseProxyCheckerLogic.test.ts new file mode 100644 index 0000000000000..5ea635b7e4f90 --- /dev/null +++ b/frontend/src/lib/components/ReverseProxyChecker/reverseProxyCheckerLogic.test.ts @@ -0,0 +1,63 @@ +import { expectLogic } from 'kea-test-utils' + +import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' + +import { reverseProxyCheckerLogic } from './reverseProxyCheckerLogic' + +const hasReverseProxyValues = [['https://proxy.example.com'], [null]] +const doesNotHaveReverseProxyValues = [[null], [null]] + +const useMockedValues = (results: (string | null)[][]): void => { + useMocks({ + post: { + '/api/projects/:team/query': () => [ + 200, + { + results, + }, + ], + }, + }) +} + +describe('reverseProxyCheckerLogic', () => { + let logic: ReturnType + + beforeEach(() => { + initKeaTests() + localStorage.clear() + logic = reverseProxyCheckerLogic() + }) + + afterEach(() => { + logic.unmount() + }) + + it('should not have a reverse proxy set - when no data', async () => { + useMockedValues([]) + + logic.mount() + await expectLogic(logic).toFinishAllListeners().toMatchValues({ + hasReverseProxy: false, + }) + }) + + it('should not have a reverse proxy set - when data with no lib_custom_api_host values', async () => { + useMockedValues(doesNotHaveReverseProxyValues) + + logic.mount() + await expectLogic(logic).toFinishAllListeners().toMatchValues({ + hasReverseProxy: false, + }) + }) + + it('should have a reverse proxy set', async () => { + useMockedValues(hasReverseProxyValues) + + logic.mount() + await expectLogic(logic).toFinishAllListeners().toMatchValues({ + hasReverseProxy: true, + }) + }) +}) diff --git a/frontend/src/lib/components/ReverseProxyChecker/reverseProxyCheckerLogic.ts b/frontend/src/lib/components/ReverseProxyChecker/reverseProxyCheckerLogic.ts new file mode 100644 index 0000000000000..6b945e5c94c48 --- /dev/null +++ b/frontend/src/lib/components/ReverseProxyChecker/reverseProxyCheckerLogic.ts @@ -0,0 +1,49 @@ +import { afterMount, kea, path, reducers } from 'kea' +import { loaders } from 'kea-loaders' +import api from 'lib/api' + +import { HogQLQuery, NodeKind } from '~/queries/schema' +import { hogql } from '~/queries/utils' + +import type { reverseProxyCheckerLogicType } from './reverseProxyCheckerLogicType' + +const CHECK_INTERVAL_MS = 1000 * 60 * 60 // 1 hour + +export const reverseProxyCheckerLogic = kea([ + path(['components', 'ReverseProxyChecker', 'reverseProxyCheckerLogic']), + loaders({ + hasReverseProxy: [ + false as boolean | null, + { + loadHasReverseProxy: async () => { + const query: HogQLQuery = { + kind: NodeKind.HogQLQuery, + query: hogql`SELECT properties.$lib_custom_api_host AS lib_custom_api_host + FROM events + WHERE timestamp >= now() - INTERVAL 1 DAY + AND timestamp <= now() + ORDER BY timestamp DESC + limit 10`, + } + + const res = await api.query(query) + return !!res.results?.find((x) => !!x[0]) + }, + }, + ], + }), + reducers({ + lastCheckedTimestamp: [ + 0, + { persist: true }, + { + loadHasReverseProxySuccess: () => Date.now(), + }, + ], + }), + afterMount(({ actions, values }) => { + if (values.lastCheckedTimestamp < Date.now() - CHECK_INTERVAL_MS) { + actions.loadHasReverseProxy() + } + }), +]) diff --git a/frontend/src/lib/components/VersionChecker/versionCheckerLogic.ts b/frontend/src/lib/components/VersionChecker/versionCheckerLogic.ts index cc26d0eff45fc..ce53ba46d5db8 100644 --- a/frontend/src/lib/components/VersionChecker/versionCheckerLogic.ts +++ b/frontend/src/lib/components/VersionChecker/versionCheckerLogic.ts @@ -7,7 +7,7 @@ import { hogql } from '~/queries/utils' import type { versionCheckerLogicType } from './versionCheckerLogicType' -const CHECK_INTERVAL_MS = 1000 * 60 * 60 // 6 hour +const CHECK_INTERVAL_MS = 1000 * 60 * 60 * 6 // 6 hour export type SDKVersion = { version: string From 93f47ade2923b0d7f1c097cb836749a71ac9aaac Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Tue, 26 Mar 2024 12:28:50 +0100 Subject: [PATCH 5/6] feat(insights): Warn about WAU/MAU in total value trends (#21067) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(insights): Warn about WAU/MAU in total value trends * Update trends.cy.ts * Reword warning * Reword warning more * Update frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx Co-authored-by: Thomas Obermüller * Update UI snapshots for `chromium` (2) * Update query snapshots * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (2) * Update query snapshots * Update UI snapshots for `chromium` (2) --------- Co-authored-by: Thomas Obermüller Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- cypress/e2e/trends.cy.ts | 34 +++++++++----- frontend/src/lib/constants.tsx | 26 ++++++---- .../lib/lemon-ui/LemonSelect/LemonSelect.tsx | 1 + frontend/src/queries/schema.json | 10 ++-- frontend/src/queries/schema.ts | 3 ++ .../filters/ActionFilter/ActionFilter.tsx | 14 +++++- .../ActionFilterRow/ActionFilterRow.tsx | 47 ++++++++++++++----- frontend/src/types.ts | 13 +++-- posthog/schema.py | 8 ++-- 9 files changed, 109 insertions(+), 47 deletions(-) diff --git a/cypress/e2e/trends.cy.ts b/cypress/e2e/trends.cy.ts index a1aa9d31a5594..36809958d7c25 100644 --- a/cypress/e2e/trends.cy.ts +++ b/cypress/e2e/trends.cy.ts @@ -24,7 +24,7 @@ describe('Trends', () => { cy.get('[data-attr=trend-element-subject-1]').click() cy.get('[data-attr=taxonomic-tab-actions]').click() cy.get('[data-attr=taxonomic-filter-searchfield]').click().type('home') - cy.contains('Hogflix homepage view').click({ force: true }) + cy.contains('Hogflix homepage view').click() // then cy.get('[data-attr=trend-line-graph]').should('exist') @@ -66,15 +66,15 @@ describe('Trends', () => { it('Apply specific filter on default pageview event', () => { cy.get('[data-attr=trend-element-subject-0]').click() cy.get('[data-attr=taxonomic-filter-searchfield]').click().type('Pageview') - cy.get('.taxonomic-infinite-list').find('.taxonomic-list-row').contains('Pageview').click({ force: true }) + cy.get('.taxonomic-infinite-list').find('.taxonomic-list-row').contains('Pageview').click() cy.get('[data-attr=trend-element-subject-0]').should('have.text', 'Pageview') // Apply a property filter cy.get('[data-attr=show-prop-filter-0]').click() cy.get('[data-attr=property-select-toggle-0]').click() - cy.get('[data-attr=prop-filter-event_properties-1]').click({ force: true }) + cy.get('[data-attr=prop-filter-event_properties-1]').click() - cy.get('[data-attr=prop-val]').click({ force: true }) + cy.get('[data-attr=prop-val]').click() // cypress is odd and even though when a human clicks this the right dropdown opens // in the test that doesn't happen cy.get('body').then(($body) => { @@ -88,14 +88,14 @@ describe('Trends', () => { it('Apply 1 overall filter', () => { cy.get('[data-attr=trend-element-subject-0]').click() cy.get('[data-attr=taxonomic-filter-searchfield]').click().type('Pageview') - cy.get('.taxonomic-infinite-list').find('.taxonomic-list-row').contains('Pageview').click({ force: true }) + cy.get('.taxonomic-infinite-list').find('.taxonomic-list-row').contains('Pageview').click() cy.get('[data-attr=trend-element-subject-0]').should('have.text', 'Pageview') cy.get('[data-attr$=add-filter-group]').click() cy.get('[data-attr=property-select-toggle-0]').click() cy.get('[data-attr=taxonomic-filter-searchfield]').click() - cy.get('[data-attr=prop-filter-event_properties-1]').click({ force: true }) - cy.get('[data-attr=prop-val]').click({ force: true }) + cy.get('[data-attr=prop-filter-event_properties-1]').click() + cy.get('[data-attr=prop-val]').click() // cypress is odd and even though when a human clicks this the right dropdown opens // in the test that doesn't happen cy.get('body').then(($body) => { @@ -103,7 +103,7 @@ describe('Trends', () => { cy.get('[data-attr=taxonomic-value-select]').click() } }) - cy.get('[data-attr=prop-val-0]').click({ force: true }) + cy.get('[data-attr=prop-val-0]').click() cy.get('[data-attr=trend-line-graph]', { timeout: 8000 }).should('exist') }) @@ -117,14 +117,14 @@ describe('Trends', () => { it('Apply pie filter', () => { cy.get('[data-attr=chart-filter]').click() - cy.get('.Popover').find('.LemonButton').contains('Pie').click({ force: true }) + cy.get('.Popover').find('.LemonButton').contains('Pie').click() cy.get('[data-attr=trend-pie-graph]').should('exist') }) it('Apply table filter', () => { cy.get('[data-attr=chart-filter]').click() - cy.get('.Popover').find('.LemonButton').contains('Table').click({ force: true }) + cy.get('.Popover').find('.LemonButton').contains('Table').click() cy.get('[data-attr=insights-table-graph]').should('exist') @@ -144,7 +144,7 @@ describe('Trends', () => { it('Apply property breakdown', () => { cy.get('[data-attr=add-breakdown-button]').click() - cy.get('[data-attr=prop-filter-event_properties-1]').click({ force: true }) + cy.get('[data-attr=prop-filter-event_properties-1]').click() cy.get('[data-attr=trend-line-graph]').should('exist') }) @@ -154,4 +154,16 @@ describe('Trends', () => { cy.contains('All Users*').click() cy.get('[data-attr=trend-line-graph]').should('exist') }) + + it('Show warning on MAU math in total value insight', () => { + cy.get('[data-attr=chart-filter]').click() + cy.get('.Popover').find('.LemonButton').contains('Pie').click() + cy.get('[data-attr=trend-pie-graph]').should('exist') // Make sure the pie chart is loaded before proceeding + + cy.get('[data-attr=math-selector-0]').click() + cy.get('[data-attr=math-monthly_active-0] .LemonIcon').should('exist') // This should be the warning icon + + cy.get('[data-attr=math-monthly_active-0]').trigger('mouseenter') // Activate warning tooltip + cy.get('.Tooltip').contains('we recommend using "Unique users" here instead').should('exist') + }) }) diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index c2d40d03957fe..292fcb5d957b7 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -1,15 +1,23 @@ import { LemonSelectOptions } from '@posthog/lemon-ui' -import { ChartDisplayType, Region, SSOProvider } from '../types' +import { ChartDisplayCategory, ChartDisplayType, Region, SSOProvider } from '../types' + +// Sync with backend DISPLAY_TYPES_TO_CATEGORIES +export const DISPLAY_TYPES_TO_CATEGORIES: Record = { + [ChartDisplayType.ActionsLineGraph]: ChartDisplayCategory.TimeSeries, + [ChartDisplayType.ActionsBar]: ChartDisplayCategory.TimeSeries, + [ChartDisplayType.ActionsAreaGraph]: ChartDisplayCategory.TimeSeries, + [ChartDisplayType.ActionsLineGraphCumulative]: ChartDisplayCategory.CumulativeTimeSeries, + [ChartDisplayType.BoldNumber]: ChartDisplayCategory.TotalValue, + [ChartDisplayType.ActionsPie]: ChartDisplayCategory.TotalValue, + [ChartDisplayType.ActionsBarValue]: ChartDisplayCategory.TotalValue, + [ChartDisplayType.ActionsTable]: ChartDisplayCategory.TotalValue, + [ChartDisplayType.WorldMap]: ChartDisplayCategory.TotalValue, +} +export const NON_TIME_SERIES_DISPLAY_TYPES = Object.entries(DISPLAY_TYPES_TO_CATEGORIES) + .filter(([, category]) => category === ChartDisplayCategory.TotalValue) + .map(([displayType]) => displayType as ChartDisplayType) -/** Display types which don't allow grouping by unit of time. Sync with backend NON_TIME_SERIES_DISPLAY_TYPES. */ -export const NON_TIME_SERIES_DISPLAY_TYPES = [ - ChartDisplayType.ActionsTable, - ChartDisplayType.ActionsPie, - ChartDisplayType.ActionsBarValue, - ChartDisplayType.WorldMap, - ChartDisplayType.BoldNumber, -] /** Display types for which `breakdown` is hidden and ignored. Sync with backend NON_BREAKDOWN_DISPLAY_TYPES. */ export const NON_BREAKDOWN_DISPLAY_TYPES = [ChartDisplayType.BoldNumber] /** Display types which only work with a single series. */ diff --git a/frontend/src/lib/lemon-ui/LemonSelect/LemonSelect.tsx b/frontend/src/lib/lemon-ui/LemonSelect/LemonSelect.tsx index 49e6c6c59190c..8e06a932310ab 100644 --- a/frontend/src/lib/lemon-ui/LemonSelect/LemonSelect.tsx +++ b/frontend/src/lib/lemon-ui/LemonSelect/LemonSelect.tsx @@ -154,6 +154,7 @@ export function LemonSelect({ } : null } + tooltip={activeLeaf?.tooltip} {...buttonProps} > diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 400ef8d1774e3..bb6e2256a43c8 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -501,14 +501,14 @@ "ChartDisplayType": { "enum": [ "ActionsLineGraph", - "ActionsLineGraphCumulative", + "ActionsBar", "ActionsAreaGraph", - "ActionsTable", + "ActionsLineGraphCumulative", + "BoldNumber", "ActionsPie", - "ActionsBar", "ActionsBarValue", - "WorldMap", - "BoldNumber" + "ActionsTable", + "WorldMap" ], "type": "string" }, diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 9bd04dd3c62a9..fc45ff6ecadcb 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -4,6 +4,7 @@ import { Breakdown, BreakdownKeyType, BreakdownType, + ChartDisplayCategory, ChartDisplayType, CountPerActorMathType, EventPropertyFilter, @@ -26,6 +27,8 @@ import { TrendsFilterType, } from '~/types' +export { ChartDisplayCategory } + // Type alias for number to be reflected as integer in json-schema. /** @asType integer */ type integer = number diff --git a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.tsx b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.tsx index b27d271f53d24..8ebb237640060 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.tsx +++ b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.tsx @@ -7,13 +7,22 @@ import { IconPlusSmall } from '@posthog/icons' import clsx from 'clsx' import { BindLogic, useActions, useValues } from 'kea' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { DISPLAY_TYPES_TO_CATEGORIES as DISPLAY_TYPES_TO_CATEGORY } from 'lib/constants' import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton' import { verticalSortableListCollisionDetection } from 'lib/sortable' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import React, { useEffect } from 'react' import { RenameModal } from 'scenes/insights/filters/ActionFilter/RenameModal' +import { isTrendsFilter } from 'scenes/insights/sharedUtils' -import { ActionFilter as ActionFilterType, FilterType, FunnelExclusionLegacy, InsightType, Optional } from '~/types' +import { + ActionFilter as ActionFilterType, + ChartDisplayType, + FilterType, + FunnelExclusionLegacy, + InsightType, + Optional, +} from '~/types' import { teamLogic } from '../../../teamLogic' import { ActionFilterRow, MathAvailability } from './ActionFilterRow/ActionFilterRow' @@ -147,6 +156,9 @@ export const ActionFilter = React.forwardRef( mathAvailability, customRowSuffix, hasBreakdown: !!filters.breakdown, + trendsDisplayCategory: isTrendsFilter(filters) + ? DISPLAY_TYPES_TO_CATEGORY[filters.display || ChartDisplayType.ActionsLineGraph] + : null, actionsTaxonomicGroupTypes, propertiesTaxonomicGroupTypes, propertyFiltersPopover, diff --git a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx index bca5a483baf48..0cb3eaeb086b3 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx +++ b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx @@ -3,7 +3,7 @@ import './ActionFilterRow.scss' import { DraggableSyntheticListeners } from '@dnd-kit/core' import { useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' -import { IconCopy, IconFilter, IconPencil, IconTrash } from '@posthog/icons' +import { IconCopy, IconFilter, IconPencil, IconTrash, IconWarning } from '@posthog/icons' import { LemonSelect, LemonSelectOption, LemonSelectOptions } from '@posthog/lemon-ui' import { BuiltLogic, useActions, useValues } from 'kea' import { EntityFilterInfo } from 'lib/components/EntityFilterInfo' @@ -39,6 +39,7 @@ import { ActionFilter, ActionFilter as ActionFilterType, BaseMathType, + ChartDisplayCategory, CountPerActorMathType, EntityType, EntityTypes, @@ -115,6 +116,7 @@ export interface ActionFilterRowProps { renameRowButton, deleteButton, }: Record) => JSX.Element // build your own row given these components + trendsDisplayCategory: ChartDisplayCategory | null } export function ActionFilterRow({ @@ -142,6 +144,7 @@ export function ActionFilterRow({ disabled = false, readOnly = false, renderRow, + trendsDisplayCategory, }: ActionFilterRowProps): JSX.Element { const { entityFilterVisible } = useValues(logic) const { @@ -377,6 +380,7 @@ export function ActionFilterRow({ disabled={readOnly} style={{ maxWidth: '100%', width: 'initial' }} mathAvailability={mathAvailability} + trendsDisplayCategory={trendsDisplayCategory} /> {mathDefinitions[math || BaseMathType.TotalCount]?.category === MathCategory.PropertyValue && ( @@ -514,6 +518,7 @@ interface MathSelectorProps { disabled?: boolean disabledReason?: string onMathSelect: (index: number, value: any) => any + trendsDisplayCategory: ChartDisplayCategory | null style?: React.CSSProperties } @@ -525,11 +530,14 @@ function isCountPerActorMath(math: string | undefined): math is CountPerActorMat return !!math && math in COUNT_PER_ACTOR_MATH_DEFINITIONS } +const TRAILING_MATH_TYPES = new Set([BaseMathType.WeeklyActiveUsers, BaseMathType.MonthlyActiveUsers]) + function useMathSelectorOptions({ math, index, mathAvailability, onMathSelect, + trendsDisplayCategory, }: MathSelectorProps): LemonSelectOptions { const mountedInsightDataLogic = insightDataLogic.findMounted() const query = mountedInsightDataLogic?.values?.query @@ -550,19 +558,33 @@ function useMathSelectorOptions({ mathAvailability != MathAvailability.ActorsOnly ? staticMathDefinitions : staticActorsOnlyMathDefinitions ) .filter(([key]) => { - if (!isStickiness) { - return true + if (isStickiness) { + // Remove WAU and MAU from stickiness insights + return !TRAILING_MATH_TYPES.has(key) + } + return true + }) + .map(([key, definition]) => { + const shouldWarnAboutTrailingMath = + TRAILING_MATH_TYPES.has(key) && trendsDisplayCategory === ChartDisplayCategory.TotalValue + return { + value: key, + icon: shouldWarnAboutTrailingMath ? : undefined, + label: definition.name, + tooltip: !shouldWarnAboutTrailingMath ? ( + definition.description + ) : ( + <> +

    {definition.description}

    + + In total value insights, it's usually not clear what date range "{definition.name}" refers + to. For full clarity, we recommend using "Unique users" here instead. + + + ), + 'data-attr': `math-${key}-${index}`, } - - // Remove WAU and MAU from stickiness insights - return key !== BaseMathType.WeeklyActiveUsers && key !== BaseMathType.MonthlyActiveUsers }) - .map(([key, definition]) => ({ - value: key, - label: definition.name, - tooltip: definition.description, - 'data-attr': `math-${key}-${index}`, - })) if (mathAvailability !== MathAvailability.ActorsOnly) { options.splice(1, 0, { @@ -580,7 +602,6 @@ function useMathSelectorOptions({ options={Object.entries(COUNT_PER_ACTOR_MATH_DEFINITIONS).map(([key, definition]) => ({ value: key, label: definition.shortName, - tooltip: definition.description, 'data-attr': `math-${key}-${index}`, }))} onClick={(e) => e.stopPropagation()} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 131598f9a79d2..58aea7f43fdab 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1838,14 +1838,19 @@ export interface DatedAnnotationType extends Omit export enum ChartDisplayType { ActionsLineGraph = 'ActionsLineGraph', - ActionsLineGraphCumulative = 'ActionsLineGraphCumulative', + ActionsBar = 'ActionsBar', ActionsAreaGraph = 'ActionsAreaGraph', - ActionsTable = 'ActionsTable', + ActionsLineGraphCumulative = 'ActionsLineGraphCumulative', + BoldNumber = 'BoldNumber', ActionsPie = 'ActionsPie', - ActionsBar = 'ActionsBar', ActionsBarValue = 'ActionsBarValue', + ActionsTable = 'ActionsTable', WorldMap = 'WorldMap', - BoldNumber = 'BoldNumber', +} +export enum ChartDisplayCategory { + TimeSeries = 'TimeSeries', + CumulativeTimeSeries = 'CumulativeTimeSeries', + TotalValue = 'TotalValue', } export type BreakdownType = 'cohort' | 'person' | 'event' | 'group' | 'session' | 'hogql' | 'data_warehouse' diff --git a/posthog/schema.py b/posthog/schema.py index 9d83587351683..7068c39090a2f 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -121,14 +121,14 @@ class ChartAxis(BaseModel): class ChartDisplayType(str, Enum): ActionsLineGraph = "ActionsLineGraph" - ActionsLineGraphCumulative = "ActionsLineGraphCumulative" + ActionsBar = "ActionsBar" ActionsAreaGraph = "ActionsAreaGraph" - ActionsTable = "ActionsTable" + ActionsLineGraphCumulative = "ActionsLineGraphCumulative" + BoldNumber = "BoldNumber" ActionsPie = "ActionsPie" - ActionsBar = "ActionsBar" ActionsBarValue = "ActionsBarValue" + ActionsTable = "ActionsTable" WorldMap = "WorldMap" - BoldNumber = "BoldNumber" class CohortPropertyFilter(BaseModel): From 822cebc5a8e44b9b930ab2db454631ccbc9382bb Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Tue, 26 Mar 2024 11:30:45 +0000 Subject: [PATCH 6/6] feat: don't clone replay event data (#21074) * feat: clone once not every * leeory j * make the tests pass --- .../session-recording/utils.ts | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/plugin-server/src/main/ingestion-queues/session-recording/utils.ts b/plugin-server/src/main/ingestion-queues/session-recording/utils.ts index 53ce953e5bd92..2c5637726743e 100644 --- a/plugin-server/src/main/ingestion-queues/session-recording/utils.ts +++ b/plugin-server/src/main/ingestion-queues/session-recording/utils.ts @@ -255,38 +255,34 @@ export const reduceRecordingMessages = (messages: IncomingRecordingMessage[]): I const reducedMessages: Record = {} for (const message of messages) { - const clonedMessage = cloneObject(message) - const key = `${clonedMessage.team_id}-${clonedMessage.session_id}` + const key = `${message.team_id}-${message.session_id}` if (!reducedMessages[key]) { - reducedMessages[key] = clonedMessage + reducedMessages[key] = cloneObject(message) } else { const existingMessage = reducedMessages[key] - for (const [windowId, events] of Object.entries(clonedMessage.eventsByWindowId)) { + for (const [windowId, events] of Object.entries(message.eventsByWindowId)) { if (existingMessage.eventsByWindowId[windowId]) { existingMessage.eventsByWindowId[windowId].push(...events) } else { existingMessage.eventsByWindowId[windowId] = events } } - existingMessage.metadata.rawSize += clonedMessage.metadata.rawSize + existingMessage.metadata.rawSize += message.metadata.rawSize // Update the events ranges existingMessage.metadata.lowOffset = Math.min( existingMessage.metadata.lowOffset, - clonedMessage.metadata.lowOffset + message.metadata.lowOffset ) existingMessage.metadata.highOffset = Math.max( existingMessage.metadata.highOffset, - clonedMessage.metadata.highOffset + message.metadata.highOffset ) // Update the events ranges - existingMessage.eventsRange.start = Math.min( - existingMessage.eventsRange.start, - clonedMessage.eventsRange.start - ) - existingMessage.eventsRange.end = Math.max(existingMessage.eventsRange.end, clonedMessage.eventsRange.end) + existingMessage.eventsRange.start = Math.min(existingMessage.eventsRange.start, message.eventsRange.start) + existingMessage.eventsRange.end = Math.max(existingMessage.eventsRange.end, message.eventsRange.end) } }