From 8bf729febe2879990e94a60eb9acf6bbb397246f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 22 Sep 2023 15:35:36 +0200 Subject: [PATCH 01/22] feat: Record S3 BatchExport errors (#17535) --- .../temporal/tests/batch_exports/fixtures.py | 16 +- .../test_bigquery_batch_export_workflow.py | 153 +++++++++++++++++- .../test_postgres_batch_export_workflow.py | 139 ++++++++++++++++ .../test_s3_batch_export_workflow.py | 150 +++++++++++++++++ .../test_snowflake_batch_export_workflow.py | 149 +++++++++++++++++ .../workflows/bigquery_batch_export.py | 19 ++- .../workflows/postgres_batch_export.py | 19 ++- posthog/temporal/workflows/s3_batch_export.py | 17 +- .../workflows/snowflake_batch_export.py | 19 ++- 9 files changed, 660 insertions(+), 21 deletions(-) diff --git a/posthog/temporal/tests/batch_exports/fixtures.py b/posthog/temporal/tests/batch_exports/fixtures.py index 65de3fd4910c3..d54db02304cc5 100644 --- a/posthog/temporal/tests/batch_exports/fixtures.py +++ b/posthog/temporal/tests/batch_exports/fixtures.py @@ -1,7 +1,13 @@ from uuid import UUID + from asgiref.sync import sync_to_async +from temporalio.client import Client -from posthog.batch_exports.models import BatchExport, BatchExportDestination, BatchExportRun +from posthog.batch_exports.models import ( + BatchExport, + BatchExportDestination, + BatchExportRun, +) from posthog.batch_exports.service import sync_batch_export @@ -32,3 +38,11 @@ def fetch_batch_export_runs(batch_export_id: UUID, limit: int = 100) -> list[Bat async def afetch_batch_export_runs(batch_export_id: UUID, limit: int = 100) -> list[BatchExportRun]: """Fetch the BatchExportRuns for a given BatchExport.""" return await sync_to_async(fetch_batch_export_runs)(batch_export_id, limit) # type: ignore + + +async def adelete_batch_export(batch_export: BatchExport, temporal: Client) -> None: + """Delete a BatchExport and its underlying Schedule.""" + handle = temporal.get_schedule_handle(str(batch_export.id)) + await handle.delete() + + await sync_to_async(batch_export.delete)() # type: ignore diff --git a/posthog/temporal/tests/batch_exports/test_bigquery_batch_export_workflow.py b/posthog/temporal/tests/batch_exports/test_bigquery_batch_export_workflow.py index b0d45d55f4b45..ad6e511577f85 100644 --- a/posthog/temporal/tests/batch_exports/test_bigquery_batch_export_workflow.py +++ b/posthog/temporal/tests/batch_exports/test_bigquery_batch_export_workflow.py @@ -1,19 +1,25 @@ +import asyncio import datetime as dt import json +import os from random import randint from uuid import uuid4 -import os import pytest +import pytest_asyncio +from asgiref.sync import sync_to_async from django.conf import settings from freezegun.api import freeze_time from google.cloud import bigquery +from temporalio import activity +from temporalio.client import WorkflowFailureError from temporalio.common import RetryPolicy from temporalio.testing import WorkflowEnvironment from temporalio.worker import UnsandboxedWorkflowRunner, Worker from posthog.api.test.test_organization import acreate_organization from posthog.api.test.test_team import acreate_team +from posthog.temporal.client import connect from posthog.temporal.tests.batch_exports.base import ( EventValues, amaterialize, @@ -21,16 +27,17 @@ ) from posthog.temporal.tests.batch_exports.fixtures import ( acreate_batch_export, + adelete_batch_export, afetch_batch_export_runs, ) from posthog.temporal.workflows.base import create_export_run, update_export_run_status -from posthog.temporal.workflows.clickhouse import ClickHouseClient from posthog.temporal.workflows.bigquery_batch_export import ( BigQueryBatchExportInputs, BigQueryBatchExportWorkflow, BigQueryInsertInputs, insert_into_bigquery_activity, ) +from posthog.temporal.workflows.clickhouse import ClickHouseClient TEST_TIME = dt.datetime.utcnow() @@ -406,3 +413,145 @@ async def test_bigquery_export_workflow( events=events, bq_ingested_timestamp=ingested_timestamp, ) + + +@pytest_asyncio.fixture +async def organization(): + organization = await acreate_organization("test") + yield organization + await sync_to_async(organization.delete)() # type: ignore + + +@pytest_asyncio.fixture +async def team(organization): + team = await acreate_team(organization=organization) + yield team + await sync_to_async(team.delete)() # type: ignore + + +@pytest_asyncio.fixture +async def batch_export(team): + destination_data = { + "type": "BigQuery", + "config": { + "table_id": f"test_workflow_table_{team.pk}", + "project_id": "project_id", + "private_key": "private_key", + "private_key_id": "private_key_id", + "token_uri": "token_uri", + "client_email": "client_email", + "dataset_id": "BatchExports", + }, + } + batch_export_data = { + "name": "my-production-bigquery-export", + "destination": destination_data, + "interval": "hour", + } + + batch_export = await acreate_batch_export( + team_id=team.pk, + name=batch_export_data["name"], + destination_data=batch_export_data["destination"], + interval=batch_export_data["interval"], + ) + + yield batch_export + + client = await connect( + settings.TEMPORAL_HOST, + settings.TEMPORAL_PORT, + settings.TEMPORAL_NAMESPACE, + settings.TEMPORAL_CLIENT_ROOT_CA, + settings.TEMPORAL_CLIENT_CERT, + settings.TEMPORAL_CLIENT_KEY, + ) + await adelete_batch_export(batch_export, client) + + +@pytest.mark.django_db +@pytest.mark.asyncio +async def test_bigquery_export_workflow_handles_insert_activity_errors(team, batch_export): + """Test that BigQuery Export Workflow can gracefully handle errors when inserting BigQuery data.""" + workflow_id = str(uuid4()) + inputs = BigQueryBatchExportInputs( + team_id=team.pk, + batch_export_id=str(batch_export.id), + data_interval_end="2023-04-25 14:30:00.000000", + **batch_export.destination.config, + ) + + @activity.defn(name="insert_into_bigquery_activity") + async def insert_into_bigquery_activity_mocked(_: BigQueryInsertInputs) -> str: + raise ValueError("A useful error message") + + async with await WorkflowEnvironment.start_time_skipping() as activity_environment: + async with Worker( + activity_environment.client, + task_queue=settings.TEMPORAL_TASK_QUEUE, + workflows=[BigQueryBatchExportWorkflow], + activities=[create_export_run, insert_into_bigquery_activity_mocked, update_export_run_status], + workflow_runner=UnsandboxedWorkflowRunner(), + ): + with pytest.raises(WorkflowFailureError): + await activity_environment.client.execute_workflow( + BigQueryBatchExportWorkflow.run, + inputs, + id=workflow_id, + task_queue=settings.TEMPORAL_TASK_QUEUE, + retry_policy=RetryPolicy(maximum_attempts=1), + ) + + runs = await afetch_batch_export_runs(batch_export_id=batch_export.id) + assert len(runs) == 1 + + run = runs[0] + assert run.status == "Failed" + assert run.latest_error == "ValueError: A useful error message" + + +@pytest.mark.django_db +@pytest.mark.asyncio +async def test_bigquery_export_workflow_handles_cancellation(team, batch_export): + """Test that BigQuery Export Workflow can gracefully handle cancellations when inserting BigQuery data.""" + workflow_id = str(uuid4()) + inputs = BigQueryBatchExportInputs( + team_id=team.pk, + batch_export_id=str(batch_export.id), + data_interval_end="2023-04-25 14:30:00.000000", + **batch_export.destination.config, + ) + + @activity.defn(name="insert_into_s3_activity") + async def never_finish_activity(_: BigQueryInsertInputs) -> str: + while True: + activity.heartbeat() + await asyncio.sleep(1) + + async with await WorkflowEnvironment.start_time_skipping() as activity_environment: + async with Worker( + activity_environment.client, + task_queue=settings.TEMPORAL_TASK_QUEUE, + workflows=[BigQueryBatchExportWorkflow], + activities=[create_export_run, never_finish_activity, update_export_run_status], + workflow_runner=UnsandboxedWorkflowRunner(), + ): + handle = await activity_environment.client.start_workflow( + BigQueryBatchExportWorkflow.run, + inputs, + id=workflow_id, + task_queue=settings.TEMPORAL_TASK_QUEUE, + retry_policy=RetryPolicy(maximum_attempts=1), + ) + await asyncio.sleep(5) + await handle.cancel() + + with pytest.raises(WorkflowFailureError): + await handle.result() + + runs = await afetch_batch_export_runs(batch_export_id=batch_export.id) + assert len(runs) == 1 + + run = runs[0] + assert run.status == "Cancelled" + assert run.latest_error == "Cancelled" diff --git a/posthog/temporal/tests/batch_exports/test_postgres_batch_export_workflow.py b/posthog/temporal/tests/batch_exports/test_postgres_batch_export_workflow.py index 499acbd29502d..831a7e9308ba1 100644 --- a/posthog/temporal/tests/batch_exports/test_postgres_batch_export_workflow.py +++ b/posthog/temporal/tests/batch_exports/test_postgres_batch_export_workflow.py @@ -1,3 +1,4 @@ +import asyncio import datetime as dt import json from random import randint @@ -5,15 +6,20 @@ import psycopg2 import pytest +import pytest_asyncio +from asgiref.sync import sync_to_async from django.conf import settings from django.test import override_settings from psycopg2 import sql +from temporalio import activity +from temporalio.client import WorkflowFailureError from temporalio.common import RetryPolicy from temporalio.testing import WorkflowEnvironment from temporalio.worker import UnsandboxedWorkflowRunner, Worker from posthog.api.test.test_organization import acreate_organization from posthog.api.test.test_team import acreate_team +from posthog.temporal.client import connect from posthog.temporal.tests.batch_exports.base import ( EventValues, amaterialize, @@ -21,6 +27,7 @@ ) from posthog.temporal.tests.batch_exports.fixtures import ( acreate_batch_export, + adelete_batch_export, afetch_batch_export_runs, ) from posthog.temporal.workflows.base import create_export_run, update_export_run_status @@ -439,3 +446,135 @@ async def test_postgres_export_workflow( assert run.status == "Completed" assert_events_in_postgres(postgres_connection, postgres_config["schema"], table_name, events) + + +@pytest_asyncio.fixture +async def organization(): + organization = await acreate_organization("test") + yield organization + await sync_to_async(organization.delete)() # type: ignore + + +@pytest_asyncio.fixture +async def team(organization): + team = await acreate_team(organization=organization) + yield team + await sync_to_async(team.delete)() # type: ignore + + +@pytest_asyncio.fixture +async def batch_export(team, postgres_config): + table_name = "test_workflow_table" + destination_data = {"type": "Postgres", "config": {**postgres_config, "table_name": table_name}} + batch_export_data = { + "name": "my-production-postgres-export", + "destination": destination_data, + "interval": "hour", + } + + batch_export = await acreate_batch_export( + team_id=team.pk, + name=batch_export_data["name"], + destination_data=batch_export_data["destination"], + interval=batch_export_data["interval"], + ) + + yield batch_export + + client = await connect( + settings.TEMPORAL_HOST, + settings.TEMPORAL_PORT, + settings.TEMPORAL_NAMESPACE, + settings.TEMPORAL_CLIENT_ROOT_CA, + settings.TEMPORAL_CLIENT_CERT, + settings.TEMPORAL_CLIENT_KEY, + ) + await adelete_batch_export(batch_export, client) + + +@pytest.mark.django_db +@pytest.mark.asyncio +async def test_postgres_export_workflow_handles_insert_activity_errors(team, batch_export): + """Test that Postgres Export Workflow can gracefully handle errors when inserting Postgres data.""" + workflow_id = str(uuid4()) + inputs = PostgresBatchExportInputs( + team_id=team.pk, + batch_export_id=str(batch_export.id), + data_interval_end="2023-04-25 14:30:00.000000", + **batch_export.destination.config, + ) + + @activity.defn(name="insert_into_postgres_activity") + async def insert_into_postgres_activity_mocked(_: PostgresInsertInputs) -> str: + raise ValueError("A useful error message") + + async with await WorkflowEnvironment.start_time_skipping() as activity_environment: + async with Worker( + activity_environment.client, + task_queue=settings.TEMPORAL_TASK_QUEUE, + workflows=[PostgresBatchExportWorkflow], + activities=[create_export_run, insert_into_postgres_activity_mocked, update_export_run_status], + workflow_runner=UnsandboxedWorkflowRunner(), + ): + with pytest.raises(WorkflowFailureError): + await activity_environment.client.execute_workflow( + PostgresBatchExportWorkflow.run, + inputs, + id=workflow_id, + task_queue=settings.TEMPORAL_TASK_QUEUE, + retry_policy=RetryPolicy(maximum_attempts=1), + ) + + runs = await afetch_batch_export_runs(batch_export_id=batch_export.id) + assert len(runs) == 1 + + run = runs[0] + assert run.status == "Failed" + assert run.latest_error == "ValueError: A useful error message" + + +@pytest.mark.django_db +@pytest.mark.asyncio +async def test_postgres_export_workflow_handles_cancellation(team, batch_export): + """Test that Postgres Export Workflow can gracefully handle cancellations when inserting Postgres data.""" + workflow_id = str(uuid4()) + inputs = PostgresBatchExportInputs( + team_id=team.pk, + batch_export_id=str(batch_export.id), + data_interval_end="2023-04-25 14:30:00.000000", + **batch_export.destination.config, + ) + + @activity.defn(name="insert_into_postgres_activity") + async def never_finish_activity(_: PostgresInsertInputs) -> str: + while True: + activity.heartbeat() + await asyncio.sleep(1) + + async with await WorkflowEnvironment.start_time_skipping() as activity_environment: + async with Worker( + activity_environment.client, + task_queue=settings.TEMPORAL_TASK_QUEUE, + workflows=[PostgresBatchExportWorkflow], + activities=[create_export_run, never_finish_activity, update_export_run_status], + workflow_runner=UnsandboxedWorkflowRunner(), + ): + handle = await activity_environment.client.start_workflow( + PostgresBatchExportWorkflow.run, + inputs, + id=workflow_id, + task_queue=settings.TEMPORAL_TASK_QUEUE, + retry_policy=RetryPolicy(maximum_attempts=1), + ) + await asyncio.sleep(5) + await handle.cancel() + + with pytest.raises(WorkflowFailureError): + await handle.result() + + runs = await afetch_batch_export_runs(batch_export_id=batch_export.id) + assert len(runs) == 1 + + run = runs[0] + assert run.status == "Cancelled" + assert run.latest_error == "Cancelled" diff --git a/posthog/temporal/tests/batch_exports/test_s3_batch_export_workflow.py b/posthog/temporal/tests/batch_exports/test_s3_batch_export_workflow.py index 08f0d285a944c..2511580358e72 100644 --- a/posthog/temporal/tests/batch_exports/test_s3_batch_export_workflow.py +++ b/posthog/temporal/tests/batch_exports/test_s3_batch_export_workflow.py @@ -1,3 +1,4 @@ +import asyncio import datetime as dt import functools import gzip @@ -12,15 +13,20 @@ import botocore.exceptions import brotli import pytest +import pytest_asyncio +from asgiref.sync import sync_to_async from django.conf import settings from django.test import Client as HttpClient from django.test import override_settings +from temporalio import activity +from temporalio.client import WorkflowFailureError from temporalio.common import RetryPolicy from temporalio.testing import WorkflowEnvironment from temporalio.worker import UnsandboxedWorkflowRunner, Worker from posthog.api.test.test_organization import acreate_organization from posthog.api.test.test_team import acreate_team +from posthog.temporal.client import connect from posthog.temporal.tests.batch_exports.base import ( EventValues, amaterialize, @@ -29,6 +35,7 @@ ) from posthog.temporal.tests.batch_exports.fixtures import ( acreate_batch_export, + adelete_batch_export, afetch_batch_export_runs, ) from posthog.temporal.workflows.base import create_export_run, update_export_run_status @@ -1072,6 +1079,149 @@ async def test_s3_export_workflow_with_minio_bucket_produces_no_duplicates( assert_events_in_s3(s3_client, bucket_name, prefix, events, compression) +@pytest_asyncio.fixture +async def organization(): + organization = await acreate_organization("test") + yield organization + await sync_to_async(organization.delete)() # type: ignore + + +@pytest_asyncio.fixture +async def team(organization): + team = await acreate_team(organization=organization) + yield team + await sync_to_async(team.delete)() # type: ignore + + +@pytest_asyncio.fixture +async def batch_export(team): + prefix = f"posthog-events-{str(uuid4())}" + destination_data = { + "type": "S3", + "config": { + "bucket_name": "test-bucket", + "region": "us-east-1", + "prefix": prefix, + "aws_access_key_id": "object_storage_root_user", + "aws_secret_access_key": "object_storage_root_password", + "compression": "gzip", + }, + } + + batch_export_data = { + "name": "my-production-s3-bucket-destination", + "destination": destination_data, + "interval": "hour", + } + + batch_export = await acreate_batch_export( + team_id=team.pk, + name=batch_export_data["name"], + destination_data=batch_export_data["destination"], + interval=batch_export_data["interval"], + ) + + yield batch_export + + client = await connect( + settings.TEMPORAL_HOST, + settings.TEMPORAL_PORT, + settings.TEMPORAL_NAMESPACE, + settings.TEMPORAL_CLIENT_ROOT_CA, + settings.TEMPORAL_CLIENT_CERT, + settings.TEMPORAL_CLIENT_KEY, + ) + await adelete_batch_export(batch_export, client) + + +@pytest.mark.django_db +@pytest.mark.asyncio +async def test_s3_export_workflow_handles_insert_activity_errors(team, batch_export): + """Test that S3 Export Workflow can gracefully handle errors when inserting S3 data.""" + workflow_id = str(uuid4()) + inputs = S3BatchExportInputs( + team_id=team.pk, + batch_export_id=str(batch_export.id), + data_interval_end="2023-04-25 14:30:00.000000", + **batch_export.destination.config, + ) + + @activity.defn(name="insert_into_s3_activity") + async def insert_into_s3_activity_mocked(_: S3InsertInputs) -> str: + raise ValueError("A useful error message") + + async with await WorkflowEnvironment.start_time_skipping() as activity_environment: + async with Worker( + activity_environment.client, + task_queue=settings.TEMPORAL_TASK_QUEUE, + workflows=[S3BatchExportWorkflow], + activities=[create_export_run, insert_into_s3_activity_mocked, update_export_run_status], + workflow_runner=UnsandboxedWorkflowRunner(), + ): + with pytest.raises(WorkflowFailureError): + await activity_environment.client.execute_workflow( + S3BatchExportWorkflow.run, + inputs, + id=workflow_id, + task_queue=settings.TEMPORAL_TASK_QUEUE, + retry_policy=RetryPolicy(maximum_attempts=1), + ) + + runs = await afetch_batch_export_runs(batch_export_id=batch_export.id) + assert len(runs) == 1 + + run = runs[0] + assert run.status == "Failed" + assert run.latest_error == "ValueError: A useful error message" + + +@pytest.mark.django_db +@pytest.mark.asyncio +async def test_s3_export_workflow_handles_cancellation(team, batch_export): + """Test that S3 Export Workflow can gracefully handle cancellations when inserting S3 data.""" + workflow_id = str(uuid4()) + inputs = S3BatchExportInputs( + team_id=team.pk, + batch_export_id=str(batch_export.id), + data_interval_end="2023-04-25 14:30:00.000000", + **batch_export.destination.config, + ) + + @activity.defn(name="insert_into_s3_activity") + async def never_finish_activity(_: S3InsertInputs) -> str: + while True: + activity.heartbeat() + await asyncio.sleep(1) + + async with await WorkflowEnvironment.start_time_skipping() as activity_environment: + async with Worker( + activity_environment.client, + task_queue=settings.TEMPORAL_TASK_QUEUE, + workflows=[S3BatchExportWorkflow], + activities=[create_export_run, never_finish_activity, update_export_run_status], + workflow_runner=UnsandboxedWorkflowRunner(), + ): + handle = await activity_environment.client.start_workflow( + S3BatchExportWorkflow.run, + inputs, + id=workflow_id, + task_queue=settings.TEMPORAL_TASK_QUEUE, + retry_policy=RetryPolicy(maximum_attempts=1), + ) + await asyncio.sleep(5) + await handle.cancel() + + with pytest.raises(WorkflowFailureError): + await handle.result() + + runs = await afetch_batch_export_runs(batch_export_id=batch_export.id) + assert len(runs) == 1 + + run = runs[0] + assert run.status == "Cancelled" + assert run.latest_error == "Cancelled" + + # We don't care about these for the next test, just need something to be defined. base_inputs = { "bucket_name": "test", diff --git a/posthog/temporal/tests/batch_exports/test_snowflake_batch_export_workflow.py b/posthog/temporal/tests/batch_exports/test_snowflake_batch_export_workflow.py index 979929d1ce205..af82608baa8a9 100644 --- a/posthog/temporal/tests/batch_exports/test_snowflake_batch_export_workflow.py +++ b/posthog/temporal/tests/batch_exports/test_snowflake_batch_export_workflow.py @@ -1,3 +1,4 @@ +import asyncio import datetime as dt import gzip import json @@ -6,10 +7,13 @@ from uuid import uuid4 import pytest +import pytest_asyncio import responses +from asgiref.sync import sync_to_async from django.conf import settings from django.test import override_settings from requests.models import PreparedRequest +from temporalio import activity from temporalio.client import WorkflowFailureError from temporalio.common import RetryPolicy from temporalio.exceptions import ActivityError, ApplicationError @@ -18,6 +22,7 @@ from posthog.api.test.test_organization import acreate_organization from posthog.api.test.test_team import acreate_team +from posthog.temporal.client import connect from posthog.temporal.tests.batch_exports.base import ( EventValues, insert_events, @@ -25,6 +30,7 @@ ) from posthog.temporal.tests.batch_exports.fixtures import ( acreate_batch_export, + adelete_batch_export, afetch_batch_export_runs, ) from posthog.temporal.workflows.base import create_export_run, update_export_run_status @@ -32,6 +38,7 @@ from posthog.temporal.workflows.snowflake_batch_export import ( SnowflakeBatchExportInputs, SnowflakeBatchExportWorkflow, + SnowflakeInsertInputs, insert_into_snowflake_activity, ) @@ -645,3 +652,145 @@ async def test_snowflake_export_workflow_raises_error_on_copy_fail(): assert isinstance(err.__cause__, ActivityError) assert isinstance(err.__cause__.__cause__, ApplicationError) assert err.__cause__.__cause__.type == "SnowflakeFileNotLoadedError" + + +@pytest_asyncio.fixture +async def organization(): + organization = await acreate_organization("test") + yield organization + await sync_to_async(organization.delete)() # type: ignore + + +@pytest_asyncio.fixture +async def team(organization): + team = await acreate_team(organization=organization) + yield team + await sync_to_async(team.delete)() # type: ignore + + +@pytest_asyncio.fixture +async def batch_export(team): + destination_data = { + "type": "Snowflake", + "config": { + "user": "hazzadous", + "password": "password", + "account": "account", + "database": "PostHog", + "schema": "test", + "warehouse": "COMPUTE_WH", + "table_name": "events", + }, + } + batch_export_data = { + "name": "my-production-snowflake-export", + "destination": destination_data, + "interval": "hour", + } + + batch_export = await acreate_batch_export( + team_id=team.pk, + name=batch_export_data["name"], + destination_data=batch_export_data["destination"], + interval=batch_export_data["interval"], + ) + + yield batch_export + + client = await connect( + settings.TEMPORAL_HOST, + settings.TEMPORAL_PORT, + settings.TEMPORAL_NAMESPACE, + settings.TEMPORAL_CLIENT_ROOT_CA, + settings.TEMPORAL_CLIENT_CERT, + settings.TEMPORAL_CLIENT_KEY, + ) + await adelete_batch_export(batch_export, client) + + +@pytest.mark.django_db +@pytest.mark.asyncio +async def test_snowflake_export_workflow_handles_insert_activity_errors(team, batch_export): + """Test that Snowflake Export Workflow can gracefully handle errors when inserting Snowflake data.""" + workflow_id = str(uuid4()) + inputs = SnowflakeBatchExportInputs( + team_id=team.pk, + batch_export_id=str(batch_export.id), + data_interval_end="2023-04-25 14:30:00.000000", + **batch_export.destination.config, + ) + + @activity.defn(name="insert_into_snowflake_activity") + async def insert_into_snowflake_activity_mocked(_: SnowflakeInsertInputs) -> str: + raise ValueError("A useful error message") + + async with await WorkflowEnvironment.start_time_skipping() as activity_environment: + async with Worker( + activity_environment.client, + task_queue=settings.TEMPORAL_TASK_QUEUE, + workflows=[SnowflakeBatchExportWorkflow], + activities=[create_export_run, insert_into_snowflake_activity_mocked, update_export_run_status], + workflow_runner=UnsandboxedWorkflowRunner(), + ): + with pytest.raises(WorkflowFailureError): + await activity_environment.client.execute_workflow( + SnowflakeBatchExportWorkflow.run, + inputs, + id=workflow_id, + task_queue=settings.TEMPORAL_TASK_QUEUE, + retry_policy=RetryPolicy(maximum_attempts=1), + ) + + runs = await afetch_batch_export_runs(batch_export_id=batch_export.id) + assert len(runs) == 1 + + run = runs[0] + assert run.status == "Failed" + assert run.latest_error == "ValueError: A useful error message" + + +@pytest.mark.django_db +@pytest.mark.asyncio +async def test_snowflake_export_workflow_handles_cancellation(team, batch_export): + """Test that Snowflake Export Workflow can gracefully handle cancellations when inserting Snowflake data.""" + workflow_id = str(uuid4()) + inputs = SnowflakeBatchExportInputs( + team_id=team.pk, + batch_export_id=str(batch_export.id), + data_interval_end="2023-04-25 14:30:00.000000", + **batch_export.destination.config, + ) + + @activity.defn(name="insert_into_snowflake_activity") + async def never_finish_activity(_: SnowflakeInsertInputs) -> str: + while True: + activity.heartbeat() + await asyncio.sleep(1) + + async with await WorkflowEnvironment.start_time_skipping() as activity_environment: + async with Worker( + activity_environment.client, + task_queue=settings.TEMPORAL_TASK_QUEUE, + workflows=[SnowflakeBatchExportWorkflow], + activities=[create_export_run, never_finish_activity, update_export_run_status], + workflow_runner=UnsandboxedWorkflowRunner(), + ): + handle = await activity_environment.client.start_workflow( + SnowflakeBatchExportWorkflow.run, + inputs, + id=workflow_id, + task_queue=settings.TEMPORAL_TASK_QUEUE, + retry_policy=RetryPolicy(maximum_attempts=1), + ) + await asyncio.sleep(5) + await handle.cancel() + + with pytest.raises(WorkflowFailureError): + await handle.result() + + runs = await afetch_batch_export_runs(batch_export_id=batch_export.id) + assert len(runs) == 1 + + run = runs[0] + assert run.status == "Cancelled" + assert run.latest_error == "Cancelled" diff --git a/posthog/temporal/workflows/bigquery_batch_export.py b/posthog/temporal/workflows/bigquery_batch_export.py index f1f247a0672fc..4be09632ff12f 100644 --- a/posthog/temporal/workflows/bigquery_batch_export.py +++ b/posthog/temporal/workflows/bigquery_batch_export.py @@ -6,7 +6,7 @@ from django.conf import settings from google.cloud import bigquery from google.oauth2 import service_account -from temporalio import activity, workflow +from temporalio import activity, exceptions, workflow from temporalio.common import RetryPolicy from posthog.batch_exports.service import BigQueryBatchExportInputs @@ -253,12 +253,21 @@ async def run(self, inputs: BigQueryBatchExportInputs): ), ) + except exceptions.ActivityError as e: + if isinstance(e.cause, exceptions.CancelledError): + workflow.logger.exception("BigQuery BatchExport was cancelled.") + update_inputs.status = "Cancelled" + else: + workflow.logger.exception("BigQuery BatchExport failed.", exc_info=e) + update_inputs.status = "Failed" + + update_inputs.latest_error = str(e.cause) + raise + except Exception as e: - workflow.logger.exception("Bigquery BatchExport failed.", exc_info=e) + workflow.logger.exception("BigQuery BatchExport failed with an unexpected exception.", exc_info=e) update_inputs.status = "Failed" - # Note: This shallows the exception type, but the message should be enough. - # If not, swap to repr(e) - update_inputs.latest_error = str(e) + update_inputs.latest_error = "An unexpected error has ocurred" raise finally: diff --git a/posthog/temporal/workflows/postgres_batch_export.py b/posthog/temporal/workflows/postgres_batch_export.py index b81c7496b3adb..b077ac892d698 100644 --- a/posthog/temporal/workflows/postgres_batch_export.py +++ b/posthog/temporal/workflows/postgres_batch_export.py @@ -6,7 +6,7 @@ import psycopg2 from django.conf import settings from psycopg2 import sql -from temporalio import activity, workflow +from temporalio import activity, exceptions, workflow from temporalio.common import RetryPolicy from posthog.batch_exports.service import PostgresBatchExportInputs @@ -254,12 +254,21 @@ async def run(self, inputs: PostgresBatchExportInputs): ), ) + except exceptions.ActivityError as e: + if isinstance(e.cause, exceptions.CancelledError): + workflow.logger.exception("Postgres BatchExport was cancelled.") + update_inputs.status = "Cancelled" + else: + workflow.logger.exception("Postgres BatchExport failed.", exc_info=e) + update_inputs.status = "Failed" + + update_inputs.latest_error = str(e.cause) + raise + except Exception as e: - workflow.logger.exception("Postgres BatchExport failed.", exc_info=e) + workflow.logger.exception("Postgres BatchExport failed with an unexpected exception.", exc_info=e) update_inputs.status = "Failed" - # Note: This shallows the exception type, but the message should be enough. - # If not, swap to repr(e) - update_inputs.latest_error = str(e) + update_inputs.latest_error = "An unexpected error has ocurred" raise finally: diff --git a/posthog/temporal/workflows/s3_batch_export.py b/posthog/temporal/workflows/s3_batch_export.py index 13bbf183e5d06..a4987b6024338 100644 --- a/posthog/temporal/workflows/s3_batch_export.py +++ b/posthog/temporal/workflows/s3_batch_export.py @@ -7,7 +7,7 @@ import boto3 from django.conf import settings -from temporalio import activity, workflow +from temporalio import activity, exceptions, workflow from temporalio.common import RetryPolicy from posthog.batch_exports.service import S3BatchExportInputs @@ -481,10 +481,21 @@ async def run(self, inputs: S3BatchExportInputs): ), ) + except exceptions.ActivityError as e: + if isinstance(e.cause, exceptions.CancelledError): + workflow.logger.exception("S3 BatchExport was cancelled.") + update_inputs.status = "Cancelled" + else: + workflow.logger.exception("S3 BatchExport failed.", exc_info=e) + update_inputs.status = "Failed" + + update_inputs.latest_error = str(e.cause) + raise + except Exception as e: - workflow.logger.exception("S3 BatchExport failed.", exc_info=e) + workflow.logger.exception("S3 BatchExport failed with an unexpected exception.", exc_info=e) update_inputs.status = "Failed" - update_inputs.latest_error = str(e) + update_inputs.latest_error = "An unexpected error has ocurred" raise finally: diff --git a/posthog/temporal/workflows/snowflake_batch_export.py b/posthog/temporal/workflows/snowflake_batch_export.py index 558e4bda7df75..a38f15d7aab73 100644 --- a/posthog/temporal/workflows/snowflake_batch_export.py +++ b/posthog/temporal/workflows/snowflake_batch_export.py @@ -6,7 +6,7 @@ import snowflake.connector from django.conf import settings from snowflake.connector.cursor import SnowflakeCursor -from temporalio import activity, workflow +from temporalio import activity, exceptions, workflow from temporalio.common import RetryPolicy from posthog.batch_exports.service import SnowflakeBatchExportInputs @@ -342,12 +342,21 @@ async def run(self, inputs: SnowflakeBatchExportInputs): ), ) + except exceptions.ActivityError as e: + if isinstance(e.cause, exceptions.CancelledError): + workflow.logger.exception("Snowflake BatchExport was cancelled.") + update_inputs.status = "Cancelled" + else: + workflow.logger.exception("Snowflake BatchExport failed.", exc_info=e) + update_inputs.status = "Failed" + + update_inputs.latest_error = str(e.cause) + raise + except Exception as e: - workflow.logger.exception("Snowflake BatchExport failed.", exc_info=e) + workflow.logger.exception("Snowflake BatchExport failed with an unexpected exception.", exc_info=e) update_inputs.status = "Failed" - # Note: This shallows the exception type, but the message should be enough. - # If not, swap to repr(e) - update_inputs.latest_error = str(e) + update_inputs.latest_error = "An unexpected error has ocurred" raise finally: From 4d8993e89d5b344b058fb4cdd6015ad13866862c Mon Sep 17 00:00:00 2001 From: David Newell Date: Fri, 22 Sep 2023 14:49:03 +0100 Subject: [PATCH 02/22] feat: bubble menu for link / text editing (#17550) --- .../__snapshots__/lemon-ui-icons--shelf-b.png | Bin 16021 -> 27772 bytes .../__snapshots__/lemon-ui-icons--shelf-i.png | Bin 30805 -> 49408 bytes frontend/src/lib/lemon-ui/icons/icons.tsx | 21 ++++++ .../notebooks/Marks/NotebookMarkLink.tsx | 29 ++++---- .../src/scenes/notebooks/Notebook/Editor.tsx | 2 + .../scenes/notebooks/Notebook/InlineMenu.tsx | 65 ++++++++++++++++++ .../scenes/notebooks/Notebook/Notebook.scss | 18 +++++ pnpm-lock.yaml | 6 +- 8 files changed, 122 insertions(+), 19 deletions(-) create mode 100644 frontend/src/scenes/notebooks/Notebook/InlineMenu.tsx diff --git a/frontend/__snapshots__/lemon-ui-icons--shelf-b.png b/frontend/__snapshots__/lemon-ui-icons--shelf-b.png index 708f32b5e4aa3869038b1c0a5cc370afe4165c05..7e81f11adc7f0b0dd86e5a08729f3487f076227f 100644 GIT binary patch literal 27772 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 GFaHbKb3>m1 literal 16021 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�QDBAdo^tg-s*U}@Hts27@lZ%Us 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 diff --git a/frontend/__snapshots__/lemon-ui-icons--shelf-i.png b/frontend/__snapshots__/lemon-ui-icons--shelf-i.png index c9f4b8b51b6b07ee3b3efcd5b5c0eb7102847559..7673cc4d1c96d273e876a18348788d44502a0de9 100644 GIT binary patch literal 49408 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 + + + ) +} + +/** Material Italic icon. */ +export function IconItalic(props: LemonIconProps): JSX.Element { + return ( + + + + ) +} + /** Material CellPhone icon. */ export function IconPhone(props: LemonIconProps): JSX.Element { return ( diff --git a/frontend/src/scenes/notebooks/Marks/NotebookMarkLink.tsx b/frontend/src/scenes/notebooks/Marks/NotebookMarkLink.tsx index 39e5feaa4aead..52a66b633af6d 100644 --- a/frontend/src/scenes/notebooks/Marks/NotebookMarkLink.tsx +++ b/frontend/src/scenes/notebooks/Marks/NotebookMarkLink.tsx @@ -1,7 +1,6 @@ -import { Mark, mergeAttributes } from '@tiptap/core' +import { Mark, getMarkRange, mergeAttributes } from '@tiptap/core' import { linkPasteRule } from '../Nodes/utils' import { Plugin, PluginKey } from '@tiptap/pm/state' -import { router } from 'kea-router' export const NotebookMarkLink = Mark.create({ name: 'link', @@ -30,28 +29,26 @@ export const NotebookMarkLink = Mark.create({ }, addProseMirrorPlugins() { + const { editor, type: markType } = this return [ new Plugin({ key: new PluginKey('handleLinkClick'), props: { handleDOMEvents: { - click(view, event) { - if (event.button !== 0) { - return false - } - - const link = event.target as HTMLAnchorElement - - const href = link.href + click(_, event) { + if (event.metaKey) { + const link = event.target as HTMLAnchorElement + const href = link.href - if (link && href && !view.editable) { - event.preventDefault() - - if (isPostHogLink(href)) { - router.actions.push(link.pathname) - } else { + if (href) { + event.preventDefault() window.open(href, link.target) } + } else { + const range = getMarkRange(editor.state.selection.$anchor, markType) + if (range) { + editor.commands.setTextSelection(range) + } } }, }, diff --git a/frontend/src/scenes/notebooks/Notebook/Editor.tsx b/frontend/src/scenes/notebooks/Notebook/Editor.tsx index 0f4e83aa16485..e814d4a314023 100644 --- a/frontend/src/scenes/notebooks/Notebook/Editor.tsx +++ b/frontend/src/scenes/notebooks/Notebook/Editor.tsx @@ -30,6 +30,7 @@ import { SlashCommandsExtension } from './SlashCommands' import { BacklinkCommandsExtension } from './BacklinkCommands' import { NotebookNodeEarlyAccessFeature } from '../Nodes/NotebookNodeEarlyAccessFeature' import { NotebookNodeSurvey } from '../Nodes/NotebookNodeSurvey' +import { InlineMenu } from './InlineMenu' const CustomDocument = ExtensionDocument.extend({ content: 'heading block*', @@ -223,6 +224,7 @@ export function Editor({ <> {_editor && } + {_editor && } ) } diff --git a/frontend/src/scenes/notebooks/Notebook/InlineMenu.tsx b/frontend/src/scenes/notebooks/Notebook/InlineMenu.tsx new file mode 100644 index 0000000000000..7a2e837c16853 --- /dev/null +++ b/frontend/src/scenes/notebooks/Notebook/InlineMenu.tsx @@ -0,0 +1,65 @@ +import { LemonButton, LemonInput } from '@posthog/lemon-ui' +import { Editor } from '@tiptap/core' +import { BubbleMenu } from '@tiptap/react' +import { IconBold, IconDelete, IconItalic, IconLink, IconOpenInNew } from 'lib/lemon-ui/icons' + +export const InlineMenu = ({ editor }: { editor: Editor }): JSX.Element => { + const { href, target } = editor.getAttributes('link') + + const setLink = (href: string): void => { + editor.commands.setMark('link', { href: href }) + } + + const openLink = (): void => { + window.open(href, target) + } + + return ( + +
+ {editor.isActive('link') ? ( + <> + + } status="primary" size="small" /> + editor.chain().focus().unsetMark('link').run()} + icon={} + status="danger" + size="small" + /> + + ) : ( + <> + editor.chain().focus().toggleMark('bold').run()} + active={editor.isActive('bold')} + icon={} + size="small" + status={editor.isActive('bold') ? 'primary' : 'stealth'} + /> + editor.chain().focus().toggleMark('italic').run()} + active={editor.isActive('italic')} + icon={} + status={editor.isActive('italic') ? 'primary' : 'stealth'} + size="small" + /> + editor.chain().focus().setMark('link').run()} + icon={} + status="stealth" + size="small" + /> + + )} +
+
+ ) +} diff --git a/frontend/src/scenes/notebooks/Notebook/Notebook.scss b/frontend/src/scenes/notebooks/Notebook/Notebook.scss index 5433abc22cff6..03675b8da8b88 100644 --- a/frontend/src/scenes/notebooks/Notebook/Notebook.scss +++ b/frontend/src/scenes/notebooks/Notebook/Notebook.scss @@ -152,6 +152,24 @@ } } + .NotebookInlineMenu { + margin-bottom: -0.2rem; + box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.1); + + .LemonInput { + border: 0px; + min-height: 0px; + } + + .LemonButton { + min-height: 1.75rem; + + .LemonButton__icon { + font-size: 1rem; + } + } + } + .NotebookNodeSettings__widgets { &__content { max-height: calc(100vh - 220px); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67d43ea804243..c8bfe1d9d3974 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5450,8 +5450,8 @@ packages: '@tiptap/core': 2.1.0-rc.12(@tiptap/pm@2.1.0-rc.12) dev: false - /@tiptap/extension-bubble-menu@2.1.0-rc.12(@tiptap/core@2.1.0-rc.12)(@tiptap/pm@2.1.0-rc.12): - resolution: {integrity: sha512-Q8DzlM61KAhrq742b0x4+Ey3WChp6X8mIvHRhNhdbChmgtNyKX1d8k72euUC6hKBCUwH4b+AQ5JVmmhoJTfsjQ==} + /@tiptap/extension-bubble-menu@2.1.10(@tiptap/core@2.1.0-rc.12)(@tiptap/pm@2.1.0-rc.12): + resolution: {integrity: sha512-XxgJajXkfAj/fChXkIwKBs7/3pd7OxV1uGc6Opx1qW/nSRYx/rr97654Sx/sg6auwIlbpRoqTmyqjbykGX1/yA==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 @@ -5652,7 +5652,7 @@ packages: react-dom: ^17.0.0 || ^18.0.0 dependencies: '@tiptap/core': 2.1.0-rc.12(@tiptap/pm@2.1.0-rc.12) - '@tiptap/extension-bubble-menu': 2.1.0-rc.12(@tiptap/core@2.1.0-rc.12)(@tiptap/pm@2.1.0-rc.12) + '@tiptap/extension-bubble-menu': 2.1.10(@tiptap/core@2.1.0-rc.12)(@tiptap/pm@2.1.0-rc.12) '@tiptap/extension-floating-menu': 2.1.0-rc.12(@tiptap/core@2.1.0-rc.12)(@tiptap/pm@2.1.0-rc.12) '@tiptap/pm': 2.1.0-rc.12 react: 16.14.0 From be05d73fee0fbd793063b3021fa3e142755ae669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 22 Sep 2023 16:56:24 +0200 Subject: [PATCH 03/22] fix(batch-exports): Do not export site url (#17588) --- posthog/temporal/tests/batch_exports/test_batch_exports.py | 6 +++--- .../batch_exports/test_bigquery_batch_export_workflow.py | 2 +- .../batch_exports/test_postgres_batch_export_workflow.py | 3 ++- posthog/temporal/workflows/batch_exports.py | 3 ++- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/posthog/temporal/tests/batch_exports/test_batch_exports.py b/posthog/temporal/tests/batch_exports/test_batch_exports.py index 50ee763b5d4d9..2bd5dd4084db1 100644 --- a/posthog/temporal/tests/batch_exports/test_batch_exports.py +++ b/posthog/temporal/tests/batch_exports/test_batch_exports.py @@ -270,7 +270,7 @@ async def test_get_results_iterator(client): "elements_chain": "this that and the other", "elements": json.dumps("this that and the other"), "ip": "127.0.0.1", - "site_url": "http://localhost.com", + "site_url": "", "set": None, "set_once": None, } @@ -327,7 +327,7 @@ async def test_get_results_iterator_handles_duplicates(client): "elements_chain": "this that and the other", "elements": json.dumps("this that and the other"), "ip": "127.0.0.1", - "site_url": "http://localhost.com", + "site_url": "", "set": None, "set_once": None, } @@ -387,7 +387,7 @@ async def test_get_results_iterator_can_exclude_events(client): "elements_chain": "this that and the other", "elements": json.dumps("this that and the other"), "ip": "127.0.0.1", - "site_url": "http://localhost.com", + "site_url": "", "set": None, "set_once": None, } diff --git a/posthog/temporal/tests/batch_exports/test_bigquery_batch_export_workflow.py b/posthog/temporal/tests/batch_exports/test_bigquery_batch_export_workflow.py index ad6e511577f85..8a843831217b8 100644 --- a/posthog/temporal/tests/batch_exports/test_bigquery_batch_export_workflow.py +++ b/posthog/temporal/tests/batch_exports/test_bigquery_batch_export_workflow.py @@ -67,7 +67,7 @@ def assert_events_in_bigquery(client, table_id, dataset_id, events, bq_ingested_ "properties": event.get("properties"), "set": properties.get("$set", None) if properties else None, "set_once": properties.get("$set_once", None) if properties else None, - "site_url": properties.get("$current_url", None) if properties else None, + "site_url": "", # For compatibility with CH which doesn't parse timezone component, so we add it here assuming UTC. "timestamp": dt.datetime.fromisoformat(event.get("timestamp") + "+00:00"), "team_id": event.get("team_id"), diff --git a/posthog/temporal/tests/batch_exports/test_postgres_batch_export_workflow.py b/posthog/temporal/tests/batch_exports/test_postgres_batch_export_workflow.py index 831a7e9308ba1..ef98f1725fab1 100644 --- a/posthog/temporal/tests/batch_exports/test_postgres_batch_export_workflow.py +++ b/posthog/temporal/tests/batch_exports/test_postgres_batch_export_workflow.py @@ -66,7 +66,8 @@ def assert_events_in_postgres(connection, schema, table_name, events): "properties": event.get("properties"), "set": properties.get("$set", None) if properties else None, "set_once": properties.get("$set_once", None) if properties else None, - "site_url": properties.get("$current_url", None) if properties else None, + # Kept for backwards compatibility, but not exported anymore. + "site_url": None, # For compatibility with CH which doesn't parse timezone component, so we add it here assuming UTC. "timestamp": dt.datetime.fromisoformat(event.get("timestamp") + "+00:00"), "team_id": event.get("team_id"), diff --git a/posthog/temporal/workflows/batch_exports.py b/posthog/temporal/workflows/batch_exports.py index c79262a0fe86a..4506069dd2bf2 100644 --- a/posthog/temporal/workflows/batch_exports.py +++ b/posthog/temporal/workflows/batch_exports.py @@ -155,7 +155,8 @@ def iter_batch_records(batch) -> typing.Generator[dict[str, typing.Any], None, N "set": properties.get("$set", None) if properties else None, "set_once": properties.get("$set_once", None) if properties else None, "properties": properties, - "site_url": properties.get("$current_url", None) if properties else None, + # Kept for backwards compatibility, but not exported anymore. + "site_url": "", "team_id": record.get("team_id"), "timestamp": record.get("timestamp").isoformat(), "uuid": record.get("uuid").decode(), From 9c1e31bfe8c44f1ead45db00cbc4e26deb3b1a23 Mon Sep 17 00:00:00 2001 From: Tom Owers Date: Fri, 22 Sep 2023 18:29:45 +0100 Subject: [PATCH 04/22] feat: hogql trends (#17519) * Scaffolding to get trends working with new runner * Handle multiple series * Added date range filters * Moved filters out of the main query * Add hogql math operator * Support hogql expressions in math operators * Fixes for master branch changes * Empty commit to trigger github checks * Remove unused import * Fixed type checking * Updated schema.json with TrendsQueryResponse * Update UI snapshots for `chromium` (1) * Update UI snapshots for `chromium` (1) * Updated schema and added series filters * Update UI snapshots for `chromium` (1) * Update UI snapshots for `chromium` (1) * Update query snapshots * Update UI snapshots for `chromium` (1) * Update query snapshots * Update UI snapshots for `chromium` (1) * Update UI snapshots for `chromium` (1) * Update UI snapshots for `chromium` (1) * Added being able to compare to previous period * Added sampling * Move query runner caching logic outside of individual runners * Added support for formulas --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- ...ordings-play-list-no-pinned-recordings.png | Bin 112684 -> 112682 bytes frontend/src/queries/schema.json | 31 ++ frontend/src/queries/schema.ts | 6 + frontend/src/queries/utils.ts | 2 +- ...ide-Bar-Hidden-Mobile-1-chromium-linux.png | Bin 3374 -> 33091 bytes ...Side-Bar-Shown-Mobile-1-chromium-linux.png | Bin 35929 -> 36042 bytes posthog/api/query.py | 5 + posthog/hogql_queries/query_runner.py | 6 +- posthog/hogql_queries/trends_query_runner.py | 311 ++++++++++++++++++ posthog/hogql_queries/utils/formula_ast.py | 67 ++++ .../utils/query_previous_period_date_range.py | 61 ++++ posthog/schema.py | 12 + 12 files changed, 497 insertions(+), 4 deletions(-) create mode 100644 posthog/hogql_queries/trends_query_runner.py create mode 100644 posthog/hogql_queries/utils/formula_ast.py create mode 100644 posthog/hogql_queries/utils/query_previous_period_date_range.py diff --git a/frontend/__snapshots__/scenes-app-recordings--recordings-play-list-no-pinned-recordings.png b/frontend/__snapshots__/scenes-app-recordings--recordings-play-list-no-pinned-recordings.png index 0dd5b12b4fe35b9e30cfa1c1dfdadaa28451e959..b79c4b316f02234f1a8fc4a14278d9a0436b145e 100644 GIT binary patch delta 34400 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 pCSD-TYL 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 YeYS(g>Cr9n6ui!!xo|r1r0%`{1<#Tx*#H0l delta 34409 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∓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 diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index ec123459bd42a..f15e50479ccdf 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -2283,6 +2283,9 @@ ], "description": "Property filters for all series" }, + "response": { + "$ref": "#/definitions/TrendsQueryResponse" + }, "samplingFactor": { "description": "Sampling rate", "type": ["number", "null"] @@ -2308,6 +2311,34 @@ }, "required": ["kind", "series"], "type": "object" + }, + "TrendsQueryResponse": { + "additionalProperties": false, + "properties": { + "is_cached": { + "type": "boolean" + }, + "last_refresh": { + "type": "string" + }, + "next_allowed_client_refresh": { + "type": "string" + }, + "result": { + "items": { + "type": "object" + }, + "type": "array" + }, + "timings": { + "items": { + "$ref": "#/definitions/QueryTiming" + }, + "type": "array" + } + }, + "required": ["result"], + "type": "object" } } } diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 2d0ad2b90c137..aa6e30283a2f5 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -374,6 +374,11 @@ export type TrendsFilter = Omit< TrendsFilterType & { hidden_legend_indexes?: number[] }, keyof FilterType | 'hidden_legend_keys' > + +export interface TrendsQueryResponse extends QueryResponse { + result: Record[] +} + export interface TrendsQuery extends InsightsQueryBase { kind: NodeKind.TrendsQuery /** Granularity of the response. Can be one of `hour`, `day`, `week` or `month` */ @@ -384,6 +389,7 @@ export interface TrendsQuery extends InsightsQueryBase { trendsFilter?: TrendsFilter /** Breakdown of the events and actions */ breakdown?: BreakdownFilter + response?: TrendsQueryResponse } /** `FunnelsFilterType` minus everything inherited from `FilterType` and diff --git a/frontend/src/queries/utils.ts b/frontend/src/queries/utils.ts index 80514ecba711b..f7da1dacf865d 100644 --- a/frontend/src/queries/utils.ts +++ b/frontend/src/queries/utils.ts @@ -126,7 +126,7 @@ export function isLifecycleQuery(node?: Node | null): node is LifecycleQuery { } export function isQueryWithHogQLSupport(node?: Node | null): node is LifecycleQuery { - return isLifecycleQuery(node) + return isLifecycleQuery(node) || isTrendsQuery(node) } export function isInsightQueryWithDisplay(node?: Node | null): node is TrendsQuery | StickinessQuery { diff --git a/playwright/e2e-vrt/layout/Navigation.spec.ts-snapshots/Navigation-App-Page-With-Side-Bar-Hidden-Mobile-1-chromium-linux.png b/playwright/e2e-vrt/layout/Navigation.spec.ts-snapshots/Navigation-App-Page-With-Side-Bar-Hidden-Mobile-1-chromium-linux.png index 24af8b22799102f55f351ab0e00aa0d3d0b7447d..0e709cd227bebd3f14dc6c5fada45208a84c96c2 100644 GIT binary patch literal 33091 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 b&+w?HVn#_}r;P0J!5I4MjPx>eb{_pd+m8tJ literal 3374 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 z

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 D7)mh^ literal 35929 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 diff --git a/posthog/api/query.py b/posthog/api/query.py index 4c714d6e6fde7..628a55da744ee 100644 --- a/posthog/api/query.py +++ b/posthog/api/query.py @@ -27,6 +27,7 @@ from posthog.hogql.query import execute_hogql_query from posthog.hogql_queries.lifecycle_query_runner import LifecycleQueryRunner +from posthog.hogql_queries.trends_query_runner import TrendsQueryRunner from posthog.models import Team from posthog.models.event.events_query import run_events_query from posthog.models.user import User @@ -227,6 +228,10 @@ def process_query( refresh_requested = refresh_requested_by_client(request) if request else False lifecycle_query_runner = LifecycleQueryRunner(query_json, team) return _unwrap_pydantic_dict(lifecycle_query_runner.run(refresh_requested=refresh_requested)) + elif query_kind == "TrendsQuery": + refresh_requested = refresh_requested_by_client(request) if request else False + trends_query_runner = TrendsQueryRunner(query_json, team) + return _unwrap_pydantic_dict(trends_query_runner.run(refresh_requested=refresh_requested)) elif query_kind == "DatabaseSchemaQuery": database = create_hogql_database(team.pk) return serialize_database(database) diff --git a/posthog/hogql_queries/query_runner.py b/posthog/hogql_queries/query_runner.py index 30a6627d9a310..5dbd4850e599d 100644 --- a/posthog/hogql_queries/query_runner.py +++ b/posthog/hogql_queries/query_runner.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from datetime import datetime, timedelta +from datetime import datetime from typing import Any, Generic, List, Optional, Type, Dict, TypeVar from prometheus_client import Counter @@ -119,9 +119,9 @@ def _cache_key(self) -> str: return generate_cache_key(f"query_{self.toJSON()}_{self.team.pk}_{self.team.timezone}") @abstractmethod - def _is_stale(self, cached_result_package) -> bool: + def _is_stale(self, cached_result_package): raise NotImplementedError() @abstractmethod - def _refresh_frequency(self) -> timedelta: + def _refresh_frequency(self): raise NotImplementedError() diff --git a/posthog/hogql_queries/trends_query_runner.py b/posthog/hogql_queries/trends_query_runner.py new file mode 100644 index 0000000000000..373b55b32790b --- /dev/null +++ b/posthog/hogql_queries/trends_query_runner.py @@ -0,0 +1,311 @@ +from datetime import timedelta +from itertools import groupby +from math import ceil +from operator import itemgetter +from typing import List, Optional, Any, Dict + +from django.utils.timezone import datetime +from posthog.caching.insights_api import BASE_MINIMUM_INSIGHT_REFRESH_INTERVAL, REDUCED_MINIMUM_INSIGHT_REFRESH_INTERVAL +from posthog.caching.utils import is_stale + +from posthog.hogql import ast +from posthog.hogql.parser import parse_expr, parse_select +from posthog.hogql.property import property_to_expr +from posthog.hogql.query import execute_hogql_query +from posthog.hogql.timings import HogQLTimings +from posthog.hogql_queries.query_runner import QueryRunner +from posthog.hogql_queries.utils.formula_ast import FormulaAST +from posthog.hogql_queries.utils.query_date_range import QueryDateRange +from posthog.hogql_queries.utils.query_previous_period_date_range import QueryPreviousPeriodDateRange +from posthog.models import Team +from posthog.models.filters.mixins.utils import cached_property +from posthog.schema import ActionsNode, EventsNode, HogQLQueryResponse, TrendsQuery, TrendsQueryResponse + + +class SeriesWithExtras: + series: EventsNode | ActionsNode + is_previous_period_series: Optional[bool] + + def __init__(self, series: EventsNode | ActionsNode, is_previous_period_series: Optional[bool]): + self.series = series + self.is_previous_period_series = is_previous_period_series + + +class TrendsQueryRunner(QueryRunner): + query: TrendsQuery + query_type = TrendsQuery + series: List[SeriesWithExtras] + + def __init__(self, query: TrendsQuery | Dict[str, Any], team: Team, timings: Optional[HogQLTimings] = None): + super().__init__(query, team, timings) + self.series = self.setup_series() + + def to_query(self) -> List[ast.SelectQuery]: + queries = [] + with self.timings.measure("trends_query"): + for series in self.series: + if not series.is_previous_period_series: + date_placeholders = self.query_date_range.to_placeholders() + else: + date_placeholders = self.query_previous_date_range.to_placeholders() + + queries.append( + parse_select( + """ + SELECT + groupArray(day_start) AS date, + groupArray(count) AS total + FROM + ( + SELECT + sum(total) AS count, + day_start + FROM + ( + SELECT + 0 AS total, + dateTrunc({interval}, {date_to}) - {number_interval_period} AS day_start + FROM + numbers( + coalesce(dateDiff({interval}, {date_from}, {date_to}), 0) + ) + UNION ALL + SELECT + 0 AS total, + {date_from} + UNION ALL + SELECT + {aggregation_operation} AS total, + dateTrunc({interval}, toTimeZone(toDateTime(timestamp), 'UTC')) AS date + FROM events AS e + %s + WHERE {events_filter} + GROUP BY date + ) + GROUP BY day_start + ORDER BY day_start ASC + ) + """ + % (self.sample_value()), + placeholders={ + **date_placeholders, + "events_filter": self.events_filter(series), + "aggregation_operation": self.aggregation_operation(series.series), + }, + timings=self.timings, + ) + ) + return queries + + def _is_stale(self, cached_result_package): + date_to = self.query_date_range.date_to() + interval = self.query_date_range.interval_name + return is_stale(self.team, date_to, interval, cached_result_package) + + def _refresh_frequency(self): + date_to = self.query_date_range.date_to() + date_from = self.query_date_range.date_from() + interval = self.query_date_range.interval_name + + delta_days: Optional[int] = None + if date_from and date_to: + delta = date_to - date_from + delta_days = ceil(delta.total_seconds() / timedelta(days=1).total_seconds()) + + refresh_frequency = BASE_MINIMUM_INSIGHT_REFRESH_INTERVAL + if interval == "hour" or (delta_days is not None and delta_days <= 7): + # The interval is shorter for short-term insights + refresh_frequency = REDUCED_MINIMUM_INSIGHT_REFRESH_INTERVAL + + return refresh_frequency + + def to_persons_query(self) -> str: + # TODO: add support for selecting and filtering by breakdowns + raise NotImplementedError() + + def calculate(self): + queries = self.to_query() + + res = [] + timings = [] + + for index, query in enumerate(queries): + series_with_extra = self.series[index] + + response = execute_hogql_query( + query_type="TrendsQuery", + query=query, + team=self.team, + timings=self.timings, + ) + + timings.extend(response.timings) + + res.extend(self.build_series_response(response, series_with_extra)) + + if self.query.trendsFilter is not None and self.query.trendsFilter.formula is not None: + res = self.apply_formula(self.query.trendsFilter.formula, res) + + return TrendsQueryResponse(result=res, timings=timings) + + def build_series_response(self, response: HogQLQueryResponse, series: SeriesWithExtras): + if response.results is None: + return [] + + res = [] + for val in response.results: + series_object = { + "data": val[1], + "labels": [item.strftime("%-d-%b-%Y") for item in val[0]], # Add back in hour formatting + "days": [item.strftime("%Y-%m-%d") for item in val[0]], # Add back in hour formatting + "count": float(sum(val[1])), + "label": "All events" if self.series_event(series.series) is None else self.series_event(series.series), + } + + # Modifications for when comparing to previous period + if self.query.trendsFilter is not None and self.query.trendsFilter.compare: + labels = [ + "{} {}".format(self.query.interval if self.query.interval is not None else "day", i) + for i in range(len(series_object["labels"])) + ] + + series_object["compare"] = True + series_object["compare_label"] = "previous" if series.is_previous_period_series else "current" + series_object["labels"] = labels + + res.append(series_object) + return res + + @cached_property + def query_date_range(self): + return QueryDateRange( + date_range=self.query.dateRange, team=self.team, interval=self.query.interval, now=datetime.now() + ) + + @cached_property + def query_previous_date_range(self): + return QueryPreviousPeriodDateRange( + date_range=self.query.dateRange, team=self.team, interval=self.query.interval, now=datetime.now() + ) + + def aggregation_operation(self, series: EventsNode | ActionsNode) -> ast.Expr: + if series.math == "hogql": + return parse_expr(series.math_hogql) + + return parse_expr("count(*)") + + def events_filter(self, series_with_extra: SeriesWithExtras) -> ast.Expr: + series = series_with_extra.series + filters: List[ast.Expr] = [] + + # Team ID + filters.append(parse_expr("team_id = {team_id}", placeholders={"team_id": ast.Constant(value=self.team.pk)})) + + if not series_with_extra.is_previous_period_series: + # Dates (current period) + filters.extend( + [ + parse_expr( + "(toTimeZone(timestamp, 'UTC') >= {date_from})", + placeholders=self.query_date_range.to_placeholders(), + ), + parse_expr( + "(toTimeZone(timestamp, 'UTC') <= {date_to})", + placeholders=self.query_date_range.to_placeholders(), + ), + ] + ) + else: + # Date (previous period) + filters.extend( + [ + parse_expr( + "(toTimeZone(timestamp, 'UTC') >= {date_from})", + placeholders=self.query_previous_date_range.to_placeholders(), + ), + parse_expr( + "(toTimeZone(timestamp, 'UTC') <= {date_to})", + placeholders=self.query_previous_date_range.to_placeholders(), + ), + ] + ) + + # Series + if self.series_event(series) is not None: + filters.append( + parse_expr("event = {event}", placeholders={"event": ast.Constant(value=self.series_event(series))}) + ) + + # Filter Test Accounts + if ( + self.query.filterTestAccounts + and isinstance(self.team.test_account_filters, list) + and len(self.team.test_account_filters) > 0 + ): + for property in self.team.test_account_filters: + filters.append(property_to_expr(property, self.team)) + + # Properties + if self.query.properties is not None and self.query.properties != []: + filters.append(property_to_expr(self.query.properties, self.team)) + + # Series Filters + if series.properties is not None and series.properties != []: + filters.append(property_to_expr(series.properties, self.team)) + + if len(filters) == 0: + return ast.Constant(value=True) + elif len(filters) == 1: + return filters[0] + else: + return ast.And(exprs=filters) + + # Using string interpolation for SAMPLE due to HogQL limitations with `UNION ALL` and `SAMPLE` AST nodes + def sample_value(self) -> str: + if self.query.samplingFactor is None: + return "" + + return f"SAMPLE {self.query.samplingFactor}" + + def series_event(self, series: EventsNode | ActionsNode) -> str | None: + if isinstance(series, EventsNode): + return series.event + return None + + def setup_series(self) -> List[SeriesWithExtras]: + if self.query.trendsFilter is not None and self.query.trendsFilter.compare: + updated_series = [] + for series in self.query.series: + updated_series.append(SeriesWithExtras(series, is_previous_period_series=False)) + updated_series.append(SeriesWithExtras(series, is_previous_period_series=True)) + return updated_series + + return [SeriesWithExtras(series, is_previous_period_series=False) for series in self.query.series] + + def apply_formula(self, formula: str, results: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + if self.query.trendsFilter is not None and self.query.trendsFilter.compare: + sorted_results = sorted(results, key=itemgetter("compare_label")) + res = [] + for _, group in groupby(sorted_results, key=itemgetter("compare_label")): + group_list = list(group) + + series_data = map(lambda s: s["data"], group_list) + new_series_data = FormulaAST(series_data).call(formula) + + new_result = group_list[0] + new_result["data"] = new_series_data + new_result["count"] = float(sum(new_series_data)) + new_result["label"] = f"Formula ({formula})" + + res.append(new_result) + return res + + series_data = map(lambda s: s["data"], results) + new_series_data = FormulaAST(series_data).call(formula) + new_result = results[0] + + new_result["data"] = new_series_data + new_result["count"] = float(sum(new_series_data)) + new_result["label"] = f"Formula ({formula})" + + return [new_result] diff --git a/posthog/hogql_queries/utils/formula_ast.py b/posthog/hogql_queries/utils/formula_ast.py new file mode 100644 index 0000000000000..95fa476d6fcee --- /dev/null +++ b/posthog/hogql_queries/utils/formula_ast.py @@ -0,0 +1,67 @@ +import ast +import operator +from typing import Any, Dict, List + + +class FormulaAST: + op_map = { + ast.Add: operator.add, + ast.Sub: operator.sub, + ast.Mult: operator.mul, + ast.Div: operator.truediv, + ast.Mod: operator.mod, + ast.Pow: operator.pow, + } + zipped_data: List[tuple[float]] + + def __init__(self, data: List[List[float]]): + self.zipped_data = list(zip(*data)) + + def call(self, node: str): + res = [] + for consts in self.zipped_data: + map = {} + for index, value in enumerate(consts): + map[chr(ord("`") + index + 1)] = value + result = self._evaluate(node.lower(), map) + res.append(result) + return res + + def _evaluate(self, node, const_map: Dict[str, Any]): + if isinstance(node, (list, tuple)): + return [self._evaluate(sub_node, const_map) for sub_node in node] + + elif isinstance(node, str): + return self._evaluate(ast.parse(node), const_map) + + elif isinstance(node, ast.Module): + values = [] + for body in node.body: + values.append(self._evaluate(body, const_map)) + if len(values) == 1: + values = values[0] + return values + + elif isinstance(node, ast.Expr): + return self._evaluate(node.value, const_map) + + elif isinstance(node, ast.BinOp): + left = self._evaluate(node.left, const_map) + op = node.op + right = self._evaluate(node.right, const_map) + + try: + return self.op_map[type(op)](left, right) + except KeyError: + raise ValueError(f"Operator {op.__class__.__name__} not supported") + + elif isinstance(node, ast.Num): + return node.n + + elif isinstance(node, ast.Name): + try: + return const_map[node.id] + except KeyError: + raise ValueError(f"Constant {node.id} not supported") + + raise TypeError(f"Unsupported operation: {node.__class__.__name__}") diff --git a/posthog/hogql_queries/utils/query_previous_period_date_range.py b/posthog/hogql_queries/utils/query_previous_period_date_range.py new file mode 100644 index 0000000000000..ac16f0b9eec10 --- /dev/null +++ b/posthog/hogql_queries/utils/query_previous_period_date_range.py @@ -0,0 +1,61 @@ +from datetime import datetime +from typing import Optional, Dict, Tuple + +from posthog.hogql_queries.utils.query_date_range import QueryDateRange +from posthog.models.team import Team +from posthog.schema import DateRange, IntervalType +from posthog.utils import get_compare_period_dates, relative_date_parse_with_delta_mapping + + +# Originally similar to posthog/queries/query_date_range.py but rewritten to be used in HogQL queries +class QueryPreviousPeriodDateRange(QueryDateRange): + """Translation of the raw `date_from` and `date_to` filter values to datetimes.""" + + _team: Team + _date_range: Optional[DateRange] + _interval: Optional[IntervalType] + _now_without_timezone: datetime + + def __init__( + self, date_range: Optional[DateRange], team: Team, interval: Optional[IntervalType], now: datetime + ) -> None: + super().__init__(date_range, team, interval, now) + + def date_from_delta_mappings(self) -> Dict[str, int] | None: + if self._date_range and isinstance(self._date_range.date_from, str) and self._date_range.date_from != "all": + delta_mapping = relative_date_parse_with_delta_mapping( + self._date_range.date_from, self._team.timezone_info, now=self.now_with_timezone + )[1] + return delta_mapping + + return None + + def date_to_delta_mappings(self) -> Dict[str, int] | None: + if self._date_range and self._date_range.date_to: + delta_mapping = relative_date_parse_with_delta_mapping( + self._date_range.date_to, self._team.timezone_info, always_truncate=True, now=self.now_with_timezone + )[1] + return delta_mapping + return None + + def dates(self) -> Tuple[datetime, datetime]: + current_period_date_from = super().date_from() + current_period_date_to = super().date_to() + + previous_period_date_from, previous_period_date_to = get_compare_period_dates( + current_period_date_from, + current_period_date_to, + self.date_from_delta_mappings(), + self.date_to_delta_mappings(), + self.interval_name, + ) + + return previous_period_date_from, previous_period_date_to + + def date_to(self) -> datetime: + previous_period_date_to = self.dates()[1] + return previous_period_date_to + + def date_from(self) -> datetime: + previous_period_date_from = self.dates()[0] + return previous_period_date_from diff --git a/posthog/schema.py b/posthog/schema.py index 214b802e27558..b80f0163d3477 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -469,6 +469,17 @@ class TrendsFilter(BaseModel): smoothing_intervals: Optional[float] = None +class TrendsQueryResponse(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + is_cached: Optional[bool] = None + last_refresh: Optional[str] = None + next_allowed_client_refresh: Optional[str] = None + result: List[Dict[str, Any]] + timings: Optional[List[QueryTiming]] = None + + class Breakdown(BaseModel): model_config = ConfigDict( extra="forbid", @@ -1111,6 +1122,7 @@ class TrendsQuery(BaseModel): PropertyGroupFilter, ] ] = Field(default=None, description="Property filters for all series") + response: Optional[TrendsQueryResponse] = None samplingFactor: Optional[float] = Field(default=None, description="Sampling rate") series: List[Union[EventsNode, ActionsNode]] = Field(..., description="Events and actions to include") trendsFilter: Optional[TrendsFilter] = Field(default=None, description="Properties specific to the trends insight") From d964adbb02ed71dbeb8405d88aaa2fd419b23f7f Mon Sep 17 00:00:00 2001 From: David Newell Date: Mon, 25 Sep 2023 07:34:54 +0100 Subject: [PATCH 05/22] chore: migrate plugins sortable (#17587) --- .../src/scenes/plugins/tabs/apps/AppView.tsx | 2 +- .../tabs/apps/InstalledAppsReorderModal.tsx | 72 +++++++++++-------- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/frontend/src/scenes/plugins/tabs/apps/AppView.tsx b/frontend/src/scenes/plugins/tabs/apps/AppView.tsx index 68065108e66c8..6c69ccb728b10 100644 --- a/frontend/src/scenes/plugins/tabs/apps/AppView.tsx +++ b/frontend/src/scenes/plugins/tabs/apps/AppView.tsx @@ -82,7 +82,7 @@ export function AppView({ } > - openReorderModal()} noPadding> + {orderedIndex ? ( ) : ( diff --git a/frontend/src/scenes/plugins/tabs/apps/InstalledAppsReorderModal.tsx b/frontend/src/scenes/plugins/tabs/apps/InstalledAppsReorderModal.tsx index 84b2c9505b107..1e5e0ad81b897 100644 --- a/frontend/src/scenes/plugins/tabs/apps/InstalledAppsReorderModal.tsx +++ b/frontend/src/scenes/plugins/tabs/apps/InstalledAppsReorderModal.tsx @@ -3,17 +3,27 @@ import { useValues, useActions } from 'kea' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' import { LemonBadge, LemonButton } from '@posthog/lemon-ui' import { PluginTypeWithConfig } from 'scenes/plugins/types' -import { SortEndHandler, SortableContainer, SortableElement } from 'react-sortable-hoc' import { PluginImage } from 'scenes/plugins/plugin/PluginImage' +import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable' +import { DndContext, DragEndEvent } from '@dnd-kit/core' +import { restrictToParentElement, restrictToVerticalAxis } from '@dnd-kit/modifiers' +import { CSS } from '@dnd-kit/utilities' const MinimalAppView = ({ plugin, order }: { plugin: PluginTypeWithConfig; order: number }): JSX.Element => { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: plugin.id }) + return (

@@ -22,36 +32,34 @@ const MinimalAppView = ({ plugin, order }: { plugin: PluginTypeWithConfig; order ) } -const SortableAppView = SortableElement(MinimalAppView) - -const SortableAppList = SortableContainer(({ children }: { children: React.ReactNode }) => { - return {children} -}) - export function InstalledAppsReorderModal(): JSX.Element { const { reorderModalOpen, sortableEnabledPlugins, temporaryOrder, pluginConfigsLoading } = useValues(pluginsLogic) const { closeReorderModal, setTemporaryOrder, cancelRearranging, savePluginOrders } = useActions(pluginsLogic) - const onSortEnd: SortEndHandler = ({ oldIndex, newIndex }) => { - const cloned = [...sortableEnabledPlugins] - const [removed] = cloned.splice(oldIndex, 1) - cloned.splice(newIndex, 0, removed) - - const newTemporaryOrder = cloned.reduce((acc, plugin, index) => { - return { - ...acc, - [plugin.id]: index + 1, - } - }, {}) - - setTemporaryOrder(newTemporaryOrder, removed.id) - } - const onClose = (): void => { cancelRearranging() closeReorderModal() } + const handleDragEnd = ({ active, over }: DragEndEvent): void => { + const itemIds = sortableEnabledPlugins.map((item) => item.id) + + if (over && active.id !== over.id) { + const oldIndex = itemIds.indexOf(Number(active.id)) + const newIndex = itemIds.indexOf(Number(over.id)) + const newOrder = arrayMove(sortableEnabledPlugins, oldIndex, newIndex) + + const newTemporaryOrder = newOrder.reduce((acc, plugin, index) => { + return { + ...acc, + [plugin.id]: index + 1, + } + }, {}) + + setTemporaryOrder(newTemporaryOrder, Number(active.id)) + } + } + return ( } > -
- - {sortableEnabledPlugins.map((plugin, index) => ( - - ))} - +
+ + + {sortableEnabledPlugins.map((item, index) => ( + + ))} + +
) From 1adf09ad00610fc452c0fe286089491963d06c0f Mon Sep 17 00:00:00 2001 From: Ben White Date: Mon, 25 Sep 2023 08:40:02 +0200 Subject: [PATCH 06/22] feat: Handle potential races in revoking replay partitions (#17578) --- .../services/partition-locker.ts | 2 +- .../session-recordings-consumer-v2.ts | 134 ++++++++++-------- .../session-recordings-consumer-v2.test.ts | 5 + 3 files changed, 80 insertions(+), 61 deletions(-) diff --git a/plugin-server/src/main/ingestion-queues/session-recording/services/partition-locker.ts b/plugin-server/src/main/ingestion-queues/session-recording/services/partition-locker.ts index 62f3200c22cfb..b97bcc1e90c67 100644 --- a/plugin-server/src/main/ingestion-queues/session-recording/services/partition-locker.ts +++ b/plugin-server/src/main/ingestion-queues/session-recording/services/partition-locker.ts @@ -83,7 +83,7 @@ export class PartitionLocker { `PartitionLocker failed to claim keys. Waiting ${this.delay} before retrying...`, { id: this.consumerID, - blockingConsumers, + blockingConsumers: [...blockingConsumers], } ) await new Promise((r) => setTimeout(r, this.delay)) diff --git a/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts b/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts index 939df4cf80f0f..51eba13ff0f8f 100644 --- a/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts +++ b/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts @@ -107,6 +107,8 @@ export class SessionRecordingIngesterV2 { recordingConsumerConfig: PluginsServerConfig topic = KAFKA_SESSION_RECORDING_SNAPSHOT_ITEM_EVENTS + private promises: Set> = new Set() + constructor( private serverConfig: PluginsServerConfig, private postgres: PostgresRouter, @@ -140,21 +142,21 @@ export class SessionRecordingIngesterV2 { this.offsetsRefresher = new BackgroundRefresher(async () => { const results = await Promise.all( - Object.keys(this.partitionAssignments).map(async (partition) => { + this.assignedTopicPartitions.map(async ({ partition }) => { return new Promise<[number, number]>((resolve, reject) => { if (!this.batchConsumer) { return reject('Not connected') } this.batchConsumer.consumer.queryWatermarkOffsets( KAFKA_SESSION_RECORDING_SNAPSHOT_ITEM_EVENTS, - parseInt(partition), + partition, (err, offsets) => { if (err) { status.error('🔥', 'Failed to query kafka watermark offsets', err) return reject() } - resolve([parseInt(partition), offsets.highOffset]) + resolve([partition, offsets.highOffset]) } ) }) @@ -168,6 +170,24 @@ export class SessionRecordingIngesterV2 { }, 5000) } + private get assignedTopicPartitions(): TopicPartition[] { + return Object.keys(this.partitionAssignments).map((partition) => ({ + partition: parseInt(partition), + topic: this.topic, + })) + } + + private scheduleWork(promise: Promise): Promise { + /** + * Helper to handle graceful shutdowns. Every time we do some work we add a promise to this array and remove it when finished. + * That way when shutting down we can wait for all promises to finish before exiting. + */ + this.promises.add(promise) + promise.finally(() => this.promises.delete(promise)) + + return promise + } + public async consume(event: IncomingRecordingMessage, sentrySpan?: Sentry.Span): Promise { // we have to reset this counter once we're consuming messages since then we know we're not re-balancing // otherwise the consumer continues to report however many sessions were revoked at the last re-balance forever @@ -426,12 +446,7 @@ export class SessionRecordingIngesterV2 { if (this.serverConfig.SESSION_RECORDING_PARTITION_REVOKE_OPTIMIZATION) { this.partitionLockInterval = setInterval(async () => { - await this.partitionLocker.claim( - Object.keys(this.partitionAssignments).map((partition) => ({ - partition: parseInt(partition), - topic: this.topic, - })) - ) + await this.partitionLocker.claim(this.assignedTopicPartitions) }, PARTITION_LOCK_INTERVAL_MS) } @@ -478,7 +493,7 @@ export class SessionRecordingIngesterV2 { } if (err.code === CODES.ERRORS.ERR__REVOKE_PARTITIONS) { - return this.onRevokePartitions(topicPartitions) + return this.scheduleWork(this.onRevokePartitions(topicPartitions)) } // We had a "real" error @@ -510,23 +525,13 @@ export class SessionRecordingIngesterV2 { await this.batchConsumer?.stop() // Simulate a revoke command to try and flush all sessions - // The rebalance event should have done this but we do it again as an extra precaution and to await the flushes - await this.onRevokePartitions( - Object.keys(this.partitionAssignments).map((partition) => ({ - partition: parseInt(partition), - topic: this.topic, - })) as TopicPartition[] - ) + // There is a race between the revoke callback and this function - Either way one of them gets there and covers the revocations + void this.scheduleWork(this.onRevokePartitions(this.assignedTopicPartitions)) await this.realtimeManager.unsubscribe() await this.replayEventsIngester.stop() - - // This is inefficient but currently necessary due to new instances restarting from the committed offset point - await this.destroySessions(Object.entries(this.sessions)) - - this.sessions = {} - - gaugeRealtimeSessions.reset() + await Promise.allSettled(this.promises) + status.info('👍', 'blob_ingester_consumer - stopped!') } public isHealthy() { @@ -554,34 +559,17 @@ export class SessionRecordingIngesterV2 { return } - const sessionsToDrop = Object.entries(this.sessions).filter(([_, sessionManager]) => - revokedPartitions.includes(sessionManager.partition) - ) - - gaugeSessionsRevoked.set(sessionsToDrop.length) - gaugeSessionsHandled.remove() + const sessionsToDrop: SessionManager[] = [] - // Attempt to flush all sessions - // TODO: Improve this to - // - work from oldest to newest - // - have some sort of timeout so we don't get stuck here forever - if (this.serverConfig.SESSION_RECORDING_PARTITION_REVOKE_OPTIMIZATION) { - status.info('🔁', `blob_ingester_consumer - flushing ${sessionsToDrop.length} sessions on revoke...`) - - await runInstrumentedFunction({ - statsKey: `recordingingester.onRevokePartitions.flushSessions`, - logExecutionTime: true, - func: async () => { - await Promise.allSettled( - sessionsToDrop - .map(([_, x]) => x) - .sort((x) => x.buffer.oldestKafkaTimestamp ?? Infinity) - .map((x) => x.flush('partition_shutdown')) - ) - }, - }) - } + // First we pull out all sessions that are being dropped. This way if we get reassigned and start consuming, we don't accidentally destroy them + Object.entries(this.sessions).forEach(([key, sessionManager]) => { + if (revokedPartitions.includes(sessionManager.partition)) { + sessionsToDrop.push(sessionManager) + delete this.sessions[key] + } + }) + // Reset all metrics for the revoked partitions topicPartitions.forEach((topicPartition: TopicPartition) => { const partition = topicPartition.partition @@ -593,11 +581,42 @@ export class SessionRecordingIngesterV2 { this.offsetHighWaterMarker.revoke(topicPartition) }) - if (this.serverConfig.SESSION_RECORDING_PARTITION_REVOKE_OPTIMIZATION) { - await this.partitionLocker.release(topicPartitions) - } - await this.destroySessions(sessionsToDrop) - await this.offsetsRefresher.refresh() + gaugeSessionsRevoked.set(sessionsToDrop.length) + gaugeSessionsHandled.remove() + + await runInstrumentedFunction({ + statsKey: `recordingingester.onRevokePartitions.revokeSessions`, + logExecutionTime: true, + timeout: 30000, // same as the partition lock + func: async () => { + if (this.serverConfig.SESSION_RECORDING_PARTITION_REVOKE_OPTIMIZATION) { + // Extend our claim on these partitions to give us time to flush + await this.partitionLocker.claim(topicPartitions) + status.info( + '🔁', + `blob_ingester_consumer - flushing ${sessionsToDrop.length} sessions on revoke...` + ) + + // Flush all the sessions we are supposed to drop + await runInstrumentedFunction({ + statsKey: `recordingingester.onRevokePartitions.flushSessions`, + logExecutionTime: true, + func: async () => { + await Promise.allSettled( + sessionsToDrop + .sort((x) => x.buffer.oldestKafkaTimestamp ?? Infinity) + .map((x) => x.flush('partition_shutdown')) + ) + }, + }) + + await this.partitionLocker.release(topicPartitions) + } + + await Promise.allSettled(sessionsToDrop.map((x) => x.destroy())) + await this.offsetsRefresher.refresh() + }, + }) } async flushAllReadySessions(): Promise { @@ -682,11 +701,6 @@ export class SessionRecordingIngesterV2 { this.partitionAssignments[partition].lastKnownCommit = highestOffsetToCommit } - status.info('💾', `blob_ingester_consumer.commitOffsets - attempting to commit offset`, { - partition, - offsetToCommit: highestOffsetToCommit, - }) - this.batchConsumer?.consumer.commit({ ...topicPartition, // see https://kafka.apache.org/10/javadoc/org/apache/kafka/clients/consumer/KafkaConsumer.html for example diff --git a/plugin-server/tests/main/ingestion-queues/session-recording/session-recordings-consumer-v2.test.ts b/plugin-server/tests/main/ingestion-queues/session-recording/session-recordings-consumer-v2.test.ts index c106c6365b2e7..e983f90d3eb23 100644 --- a/plugin-server/tests/main/ingestion-queues/session-recording/session-recordings-consumer-v2.test.ts +++ b/plugin-server/tests/main/ingestion-queues/session-recording/session-recordings-consumer-v2.test.ts @@ -409,6 +409,11 @@ describe('ingester', () => { otherIngester.onAssignPartitions([createTP(2), createTP(3)]), ] + // Should immediately be removed from the tracked sessions + expect( + Object.values(ingester.sessions).map((x) => `${x.partition}:${x.sessionId}:${x.buffer.count}`) + ).toEqual(['1:session_id_1:1', '1:session_id_2:1']) + // Call the second ingester to receive the messages. The revocation should still be in progress meaning they are "paused" for a bit // Once the revocation is complete the second ingester should receive the messages but drop most of them as they got flushes by the revoke await otherIngester.handleEachBatch([ From d9388ad3bda2185a901d1f0f3584bc8b756b414e Mon Sep 17 00:00:00 2001 From: Ben White Date: Mon, 25 Sep 2023 09:13:15 +0200 Subject: [PATCH 07/22] feat: Added option for parallel processing of replay ingestion (#17585) --- plugin-server/src/config/config.ts | 1 + .../session-recordings-consumer-v2.ts | 23 ++++++++----------- plugin-server/src/types.ts | 1 + 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/plugin-server/src/config/config.ts b/plugin-server/src/config/config.ts index 02ee642c6648f..afca924b3cb1f 100644 --- a/plugin-server/src/config/config.ts +++ b/plugin-server/src/config/config.ts @@ -152,6 +152,7 @@ export function getDefaultConfig(): PluginsServerConfig { SESSION_RECORDING_REMOTE_FOLDER: 'session_recordings', SESSION_RECORDING_REDIS_PREFIX: '@posthog/replay/', SESSION_RECORDING_PARTITION_REVOKE_OPTIMIZATION: false, + SESSION_RECORDING_PARALLEL_CONSUMPTION: false, POSTHOG_SESSION_RECORDING_REDIS_HOST: undefined, POSTHOG_SESSION_RECORDING_REDIS_PORT: undefined, } diff --git a/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts b/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts index 51eba13ff0f8f..fd8115a30988f 100644 --- a/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts +++ b/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts @@ -332,7 +332,6 @@ export class SessionRecordingIngesterV2 { statsKey: `recordingingester.handleEachBatch`, logExecutionTime: true, func: async () => { - const transaction = Sentry.startTransaction({ name: `blobIngestion_handleEachBatch` }, {}) histogramKafkaBatchSize.observe(messages.length) const recordingMessages: IncomingRecordingMessage[] = [] @@ -385,16 +384,14 @@ export class SessionRecordingIngesterV2 { }) await runInstrumentedFunction({ - statsKey: `recordingingester.handleEachBatch.consumeSerial`, + statsKey: `recordingingester.handleEachBatch.consumeBatch`, func: async () => { - for (const message of recordingMessages) { - const consumeSpan = transaction?.startChild({ - op: 'blobConsume', - }) - - await this.consume(message, consumeSpan) - // TODO: We could do this as batch of offsets for the whole lot... - consumeSpan?.finish() + if (this.serverConfig.SESSION_RECORDING_PARALLEL_CONSUMPTION) { + await Promise.all(recordingMessages.map((x) => this.consume(x))) + } else { + for (const message of recordingMessages) { + await this.consume(message) + } } }, }) @@ -417,8 +414,6 @@ export class SessionRecordingIngesterV2 { await this.flushAllReadySessions() }, }) - - transaction.finish() }, }) } @@ -544,7 +539,9 @@ export class SessionRecordingIngesterV2 { this.partitionAssignments[topicPartition.partition] = {} }) - await this.partitionLocker.claim(topicPartitions) + if (this.serverConfig.SESSION_RECORDING_PARTITION_REVOKE_OPTIMIZATION) { + await this.partitionLocker.claim(topicPartitions) + } await this.offsetsRefresher.refresh() } diff --git a/plugin-server/src/types.ts b/plugin-server/src/types.ts index 12f76ce214378..f6c2c9077c5ac 100644 --- a/plugin-server/src/types.ts +++ b/plugin-server/src/types.ts @@ -221,6 +221,7 @@ export interface PluginsServerConfig { SESSION_RECORDING_REMOTE_FOLDER: string SESSION_RECORDING_REDIS_PREFIX: string SESSION_RECORDING_PARTITION_REVOKE_OPTIMIZATION: boolean + SESSION_RECORDING_PARALLEL_CONSUMPTION: boolean // Dedicated infra values SESSION_RECORDING_KAFKA_HOSTS: string | undefined From cbe3a959dfc193f416bc0dfc8dbfc5bd928026dd Mon Sep 17 00:00:00 2001 From: Ben White Date: Mon, 25 Sep 2023 12:41:32 +0200 Subject: [PATCH 08/22] fix: Wait for ingester shutdown before redis shutdown (#17594) --- .../services/partition-locker.ts | 1 + .../services/realtime-manager.ts | 1 + .../session-recordings-consumer-v2.ts | 23 +++++-- plugin-server/src/main/pluginsServer.ts | 15 +--- .../session-recordings-consumer-v2.test.ts | 69 +++++++++++++++++-- 5 files changed, 83 insertions(+), 26 deletions(-) diff --git a/plugin-server/src/main/ingestion-queues/session-recording/services/partition-locker.ts b/plugin-server/src/main/ingestion-queues/session-recording/services/partition-locker.ts index b97bcc1e90c67..cdc56f5efabdf 100644 --- a/plugin-server/src/main/ingestion-queues/session-recording/services/partition-locker.ts +++ b/plugin-server/src/main/ingestion-queues/session-recording/services/partition-locker.ts @@ -131,6 +131,7 @@ export class PartitionLocker { keys, }, }) + throw error } } } diff --git a/plugin-server/src/main/ingestion-queues/session-recording/services/realtime-manager.ts b/plugin-server/src/main/ingestion-queues/session-recording/services/realtime-manager.ts index 7571ed0835f53..e2c4d50d79bf9 100644 --- a/plugin-server/src/main/ingestion-queues/session-recording/services/realtime-manager.ts +++ b/plugin-server/src/main/ingestion-queues/session-recording/services/realtime-manager.ts @@ -66,6 +66,7 @@ export class RealtimeManager extends EventEmitter { ) this.pubsubRedis?.disconnect() + this.pubsubRedis = undefined } private async run(description: string, fn: (client: Redis) => Promise): Promise { diff --git a/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts b/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts index fd8115a30988f..df9437de004b5 100644 --- a/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts +++ b/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts @@ -13,6 +13,7 @@ import { PipelineEvent, PluginsServerConfig, RawEventMessage, RedisPool, TeamId import { BackgroundRefresher } from '../../../utils/background-refresher' import { PostgresRouter } from '../../../utils/db/postgres' import { status } from '../../../utils/status' +import { createRedisPool } from '../../../utils/utils' import { fetchTeamTokensWithRecordings } from '../../../worker/ingestion/team-manager' import { ObjectStorage } from '../../services/object_storage' import { addSentryBreadcrumbsEventListeners } from '../kafka-metrics' @@ -94,6 +95,7 @@ type PartitionMetrics = { } export class SessionRecordingIngesterV2 { + redisPool: RedisPool sessions: Record = {} offsetHighWaterMarker: OffsetHighWaterMarker realtimeManager: RealtimeManager @@ -112,10 +114,11 @@ export class SessionRecordingIngesterV2 { constructor( private serverConfig: PluginsServerConfig, private postgres: PostgresRouter, - private objectStorage: ObjectStorage, - private redisPool: RedisPool + private objectStorage: ObjectStorage ) { this.recordingConsumerConfig = sessionRecordingConsumerConfig(this.serverConfig) + this.redisPool = createRedisPool(this.serverConfig) + this.realtimeManager = new RealtimeManager(this.redisPool, this.recordingConsumerConfig) this.partitionLocker = new PartitionLocker( this.redisPool, @@ -509,24 +512,30 @@ export class SessionRecordingIngesterV2 { }) } - public async stop(): Promise { + public async stop(): Promise[]> { status.info('🔁', 'blob_ingester_consumer - stopping') if (this.partitionLockInterval) { clearInterval(this.partitionLockInterval) } - // Mark as stopping so that we don't actually process any more incoming messages, but still keep the process alive await this.batchConsumer?.stop() // Simulate a revoke command to try and flush all sessions // There is a race between the revoke callback and this function - Either way one of them gets there and covers the revocations void this.scheduleWork(this.onRevokePartitions(this.assignedTopicPartitions)) + void this.scheduleWork(this.realtimeManager.unsubscribe()) + void this.scheduleWork(this.replayEventsIngester.stop()) + + const promiseResults = await Promise.allSettled(this.promises) + + // Finally we clear up redis once we are sure everything else has been handled + await this.redisPool.drain() + await this.redisPool.clear() - await this.realtimeManager.unsubscribe() - await this.replayEventsIngester.stop() - await Promise.allSettled(this.promises) status.info('👍', 'blob_ingester_consumer - stopped!') + + return promiseResults } public isHealthy() { diff --git a/plugin-server/src/main/pluginsServer.ts b/plugin-server/src/main/pluginsServer.ts index eef7fdaa8b6de..30ef80768f985 100644 --- a/plugin-server/src/main/pluginsServer.ts +++ b/plugin-server/src/main/pluginsServer.ts @@ -17,7 +17,7 @@ import { captureEventLoopMetrics } from '../utils/metrics' import { cancelAllScheduledJobs } from '../utils/node-schedule' import { PubSub } from '../utils/pubsub' import { status } from '../utils/status' -import { createRedisPool, delay } from '../utils/utils' +import { delay } from '../utils/utils' import { OrganizationManager } from '../worker/ingestion/organization-manager' import { TeamManager } from '../worker/ingestion/team-manager' import Piscina, { makePiscina as defaultMakePiscina } from '../worker/piscina' @@ -420,27 +420,18 @@ export async function startPluginsServer( const statsd = hub?.statsd ?? createStatsdClient(serverConfig, null) const postgres = hub?.postgres ?? new PostgresRouter(serverConfig, statsd) const s3 = hub?.objectStorage ?? getObjectStorage(recordingConsumerConfig) - const redisPool = hub?.db.redisPool ?? createRedisPool(recordingConsumerConfig) if (!s3) { throw new Error("Can't start session recording blob ingestion without object storage") } // NOTE: We intentionally pass in the original serverConfig as the ingester uses both kafkas - const ingester = new SessionRecordingIngesterV2(serverConfig, postgres, s3, redisPool) + const ingester = new SessionRecordingIngesterV2(serverConfig, postgres, s3) await ingester.start() const batchConsumer = ingester.batchConsumer if (batchConsumer) { - stopSessionRecordingBlobConsumer = async () => { - // Tricky - in some cases the hub is responsible, in which case it will drain and clear. Otherwise we are responsible. - if (!hub?.db.redisPool) { - await redisPool.drain() - await redisPool.clear() - } - - await ingester.stop() - } + stopSessionRecordingBlobConsumer = () => ingester.stop() joinSessionRecordingBlobConsumer = () => batchConsumer.join() healthChecks['session-recordings-blob'] = () => ingester.isHealthy() ?? false } diff --git a/plugin-server/tests/main/ingestion-queues/session-recording/session-recordings-consumer-v2.test.ts b/plugin-server/tests/main/ingestion-queues/session-recording/session-recordings-consumer-v2.test.ts index e983f90d3eb23..53cc8f019d861 100644 --- a/plugin-server/tests/main/ingestion-queues/session-recording/session-recordings-consumer-v2.test.ts +++ b/plugin-server/tests/main/ingestion-queues/session-recording/session-recordings-consumer-v2.test.ts @@ -72,6 +72,9 @@ describe('ingester', () => { const team = await getFirstTeam(hub) teamToken = team.api_token await deleteKeysWithPrefix(hub) + + ingester = new SessionRecordingIngesterV2(config, hub.postgres, hub.objectStorage) + await ingester.start() }) afterEach(async () => { @@ -86,12 +89,6 @@ describe('ingester', () => { jest.useRealTimers() }) - // these tests assume that a flush won't run while they run - beforeEach(async () => { - ingester = new SessionRecordingIngesterV2(config, hub.postgres, hub.objectStorage, hub.redisPool) - await ingester.start() - }) - it('creates a new session manager if needed', async () => { const event = createIncomingRecordingMessage() await ingester.consume(event) @@ -339,7 +336,7 @@ describe('ingester', () => { jest.setTimeout(5000) // Increased to cover lock delay beforeEach(async () => { - otherIngester = new SessionRecordingIngesterV2(config, hub.postgres, hub.objectStorage, hub.redisPool) + otherIngester = new SessionRecordingIngesterV2(config, hub.postgres, hub.objectStorage) await otherIngester.start() }) @@ -443,4 +440,62 @@ describe('ingester', () => { ).toEqual(['2:session_id_4:1']) }) }) + + describe('stop()', () => { + const setup = async (): Promise => { + const partitionMsgs1 = [ + createKafkaMessage( + teamToken, + { + partition: 1, + offset: 1, + }, + { + $session_id: 'session_id_1', + } + ), + + createKafkaMessage( + teamToken, + { + partition: 1, + offset: 2, + }, + { + $session_id: 'session_id_2', + } + ), + ] + + await ingester.onAssignPartitions([createTP(1)]) + await ingester.handleEachBatch(partitionMsgs1) + } + + // NOTE: This test is a sanity check for the follow up test. It demonstrates what happens if we shutdown in the wrong order + // It doesn't reliably work though as the onRevoke is called via the kafka lib ending up with dangling promises so rather it is here as a reminder + // demonstation for when we need it + it.skip('shuts down with error if redis forcefully shutdown', async () => { + await setup() + + await ingester.redisPool.drain() + await ingester.redisPool.clear() + + // revoke, realtime unsub, replay stop + await expect(ingester.stop()).resolves.toMatchObject([ + { status: 'rejected' }, + { status: 'fulfilled' }, + { status: 'fulfilled' }, + ]) + }) + it('shuts down without error', async () => { + await setup() + + // revoke, realtime unsub, replay stop + await expect(ingester.stop()).resolves.toMatchObject([ + { status: 'fulfilled' }, + { status: 'fulfilled' }, + { status: 'fulfilled' }, + ]) + }) + }) }) From c7d06af105427507472d8a75380912b154a2c8a6 Mon Sep 17 00:00:00 2001 From: Ben White Date: Mon, 25 Sep 2023 13:54:14 +0200 Subject: [PATCH 09/22] fix: Redis server for replay (#17597) --- .../session-recording/session-recordings-consumer-v2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts b/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts index df9437de004b5..950eb20f8afcf 100644 --- a/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts +++ b/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts @@ -117,7 +117,7 @@ export class SessionRecordingIngesterV2 { private objectStorage: ObjectStorage ) { this.recordingConsumerConfig = sessionRecordingConsumerConfig(this.serverConfig) - this.redisPool = createRedisPool(this.serverConfig) + this.redisPool = createRedisPool(this.recordingConsumerConfig) this.realtimeManager = new RealtimeManager(this.redisPool, this.recordingConsumerConfig) this.partitionLocker = new PartitionLocker( From bf1a80deb4b345640c5bb44d213d1bf831ed4bd9 Mon Sep 17 00:00:00 2001 From: Tiina Turban Date: Mon, 25 Sep 2023 15:34:29 +0200 Subject: [PATCH 10/22] chore: App metrics no hub no promise manager (#17581) --- plugin-server/src/config/config.ts | 1 + plugin-server/src/types.ts | 1 + plugin-server/src/utils/db/hub.ts | 10 ++- .../src/worker/ingestion/app-metrics.ts | 85 ++++++++----------- .../worker/ingestion/app-metrics.test.ts | 66 ++++++-------- 5 files changed, 76 insertions(+), 87 deletions(-) diff --git a/plugin-server/src/config/config.ts b/plugin-server/src/config/config.ts index afca924b3cb1f..a6d9a373b4696 100644 --- a/plugin-server/src/config/config.ts +++ b/plugin-server/src/config/config.ts @@ -62,6 +62,7 @@ export function getDefaultConfig(): PluginsServerConfig { KAFKA_MAX_MESSAGE_BATCH_SIZE: isDevEnv() ? 0 : 900_000, KAFKA_FLUSH_FREQUENCY_MS: isTestEnv() ? 5 : 500, APP_METRICS_FLUSH_FREQUENCY_MS: isTestEnv() ? 5 : 20_000, + APP_METRICS_FLUSH_MAX_QUEUE_SIZE: isTestEnv() ? 5 : 1000, REDIS_URL: 'redis://127.0.0.1', POSTHOG_REDIS_PASSWORD: '', POSTHOG_REDIS_HOST: '', diff --git a/plugin-server/src/types.ts b/plugin-server/src/types.ts index f6c2c9077c5ac..b9bfe64dce03d 100644 --- a/plugin-server/src/types.ts +++ b/plugin-server/src/types.ts @@ -145,6 +145,7 @@ export interface PluginsServerConfig { KAFKA_MAX_MESSAGE_BATCH_SIZE: number KAFKA_FLUSH_FREQUENCY_MS: number APP_METRICS_FLUSH_FREQUENCY_MS: number + APP_METRICS_FLUSH_MAX_QUEUE_SIZE: number BASE_DIR: string // base path for resolving local plugins PLUGINS_RELOAD_PUBSUB_CHANNEL: string // Redis channel for reload events' LOG_LEVEL: LogLevel diff --git a/plugin-server/src/utils/db/hub.ts b/plugin-server/src/utils/db/hub.ts index aeb5c26c95cfa..2ae134ae1fb6a 100644 --- a/plugin-server/src/utils/db/hub.ts +++ b/plugin-server/src/utils/db/hub.ts @@ -28,6 +28,7 @@ import { AppMetrics } from '../../worker/ingestion/app-metrics' import { OrganizationManager } from '../../worker/ingestion/organization-manager' import { EventsProcessor } from '../../worker/ingestion/process-event' import { TeamManager } from '../../worker/ingestion/team-manager' +import { isTestEnv } from '../env-utils' import { status } from '../status' import { createRedisPool, UUIDT } from '../utils' import { PluginsApiKeyManager } from './../../worker/vm/extensions/helpers/api-key-manager' @@ -192,9 +193,16 @@ export async function createHub( // :TODO: This is only used on worker threads, not main hub.eventsProcessor = new EventsProcessor(hub as Hub) - hub.appMetrics = new AppMetrics(hub as Hub) + hub.appMetrics = new AppMetrics( + kafkaProducer, + serverConfig.APP_METRICS_FLUSH_FREQUENCY_MS, + serverConfig.APP_METRICS_FLUSH_MAX_QUEUE_SIZE + ) const closeHub = async () => { + if (!isTestEnv()) { + await hub.appMetrics?.flush() + } await Promise.allSettled([kafkaProducer.disconnect(), redisPool.drain(), hub.postgres?.end()]) await redisPool.clear() diff --git a/plugin-server/src/worker/ingestion/app-metrics.ts b/plugin-server/src/worker/ingestion/app-metrics.ts index a52345df75a31..333104e967d4a 100644 --- a/plugin-server/src/worker/ingestion/app-metrics.ts +++ b/plugin-server/src/worker/ingestion/app-metrics.ts @@ -2,9 +2,10 @@ import * as Sentry from '@sentry/node' import { Message } from 'kafkajs' import { DateTime } from 'luxon' import { configure } from 'safe-stable-stringify' +import { KafkaProducerWrapper } from 'utils/db/kafka-producer-wrapper' import { KAFKA_APP_METRICS } from '../../config/kafka-topics' -import { Hub, TeamId, TimestampFormat } from '../../types' +import { TeamId, TimestampFormat } from '../../types' import { cleanErrorStackTrace } from '../../utils/db/error' import { status } from '../../utils/status' import { castTimestampOrNow, UUIDT } from '../../utils/utils' @@ -61,52 +62,43 @@ const safeJSONStringify = configure({ }) export class AppMetrics { - hub: Hub + kafkaProducer: KafkaProducerWrapper queuedData: Record flushFrequencyMs: number + maxQueueSize: number - timer: NodeJS.Timeout | null + lastFlushTime: number + // For quick access to queueSize instead of using Object.keys(queuedData).length every time + queueSize: number - constructor(hub: Hub) { - this.hub = hub + constructor(kafkaProducer: KafkaProducerWrapper, flushFrequencyMs: number, maxQueueSize: number) { this.queuedData = {} - this.flushFrequencyMs = hub.APP_METRICS_FLUSH_FREQUENCY_MS - this.timer = null + this.kafkaProducer = kafkaProducer + this.flushFrequencyMs = flushFrequencyMs + this.maxQueueSize = maxQueueSize + this.lastFlushTime = Date.now() + this.queueSize = 0 } - async isAvailable(metric: AppMetric, errorWithContext?: ErrorWithContext): Promise { - if (this.hub.APP_METRICS_GATHERED_FOR_ALL) { - return true - } - - // :TRICKY: If postgres connection is down, we ignore this metric - try { - return await this.hub.organizationManager.hasAvailableFeature(metric.teamId, 'app_metrics') - } catch (err) { - status.warn( - '⚠️', - 'Error querying whether app_metrics is available. Ignoring this metric', - metric, - errorWithContext, - err - ) - return false + async queueMetric(metric: AppMetric, timestamp?: number): Promise { + // We don't want to immediately flush all the metrics every time as we can internally + // aggregate them quite a bit and reduce the message count by a lot. + // However, we also don't want to wait too long, nor have the queue grow too big resulting in + // the flush taking a long time. + const now = Date.now() + if (now - this.lastFlushTime > this.flushFrequencyMs || this.queueSize > this.maxQueueSize) { + await this.flush() } - } - async queueMetric(metric: AppMetric, timestamp?: number): Promise { - timestamp = timestamp || Date.now() + timestamp = timestamp || now const key = this._key(metric) - if (!(await this.isAvailable(metric))) { - return - } - const { successes, successesOnRetry, failures, errorUuid, errorType, errorDetails, ...metricInfo } = metric if (!this.queuedData[key]) { + this.queueSize += 1 this.queuedData[key] = { successes: 0, successesOnRetry: 0, @@ -131,33 +123,29 @@ export class AppMetrics { this.queuedData[key].failures += failures } this.queuedData[key].lastTimestamp = timestamp - - if (this.timer === null) { - this.timer = setTimeout(() => { - this.hub.promiseManager.trackPromise(this.flush(), 'app metrics') - this.timer = null - }, this.flushFrequencyMs) - } } async queueError(metric: AppMetric, errorWithContext: ErrorWithContext, timestamp?: number) { - if (await this.isAvailable(metric, errorWithContext)) { - await this.queueMetric( - { - ...metric, - ...this._metricErrorParameters(errorWithContext), - }, - timestamp - ) - } + await this.queueMetric( + { + ...metric, + ...this._metricErrorParameters(errorWithContext), + }, + timestamp + ) } async flush(): Promise { + console.log(`Flushing app metrics`) + const startTime = Date.now() + this.lastFlushTime = startTime if (Object.keys(this.queuedData).length === 0) { return } + // TODO: We might be dropping some metrics here if someone wrote between queue assigment and queuedData={} assignment const queue = this.queuedData + this.queueSize = 0 this.queuedData = {} const kafkaMessages: Message[] = Object.values(queue).map((value) => ({ @@ -178,10 +166,11 @@ export class AppMetrics { }), })) - await this.hub.kafkaProducer.queueMessage({ + await this.kafkaProducer.queueMessage({ topic: KAFKA_APP_METRICS, messages: kafkaMessages, }) + console.log(`Finisehd flushing app metrics, took ${Date.now() - startTime}ms`) } _metricErrorParameters(errorWithContext: ErrorWithContext): Partial { diff --git a/plugin-server/tests/worker/ingestion/app-metrics.test.ts b/plugin-server/tests/worker/ingestion/app-metrics.test.ts index c46f07998f460..43a2b07364208 100644 --- a/plugin-server/tests/worker/ingestion/app-metrics.test.ts +++ b/plugin-server/tests/worker/ingestion/app-metrics.test.ts @@ -23,18 +23,18 @@ describe('AppMetrics()', () => { let closeHub: () => Promise beforeEach(async () => { - ;[hub, closeHub] = await createHub({ APP_METRICS_FLUSH_FREQUENCY_MS: 100 }) - appMetrics = new AppMetrics(hub) - - jest.spyOn(hub.organizationManager, 'hasAvailableFeature').mockResolvedValue(true) + ;[hub, closeHub] = await createHub({ APP_METRICS_FLUSH_FREQUENCY_MS: 100, APP_METRICS_FLUSH_MAX_QUEUE_SIZE: 5 }) + appMetrics = new AppMetrics( + hub.kafkaProducer, + hub.APP_METRICS_FLUSH_FREQUENCY_MS, + hub.APP_METRICS_FLUSH_MAX_QUEUE_SIZE + ) + // doesn't flush again on the next call, i.e. flust metrics were reset jest.spyOn(hub.kafkaProducer, 'queueMessage').mockReturnValue(Promise.resolve()) }) afterEach(async () => { jest.useRealTimers() - if (appMetrics.timer) { - clearTimeout(appMetrics.timer) - } await closeHub() }) @@ -164,44 +164,34 @@ describe('AppMetrics()', () => { ]) }) - it('creates timer to flush if no timer before', async () => { - jest.spyOn(appMetrics, 'flush') - jest.useFakeTimers() - - await appMetrics.queueMetric({ ...metric, successes: 1 }, timestamp) - - const timer = appMetrics.timer - expect(timer).not.toBeNull() - - jest.advanceTimersByTime(120) + it('flushes when time is up', async () => { + Date.now = jest.fn(() => 1600000000) + await appMetrics.flush() - expect(appMetrics.timer).toBeNull() - expect(appMetrics.flush).toHaveBeenCalled() - }) + jest.spyOn(appMetrics, 'flush') + Date.now = jest.fn(() => 1600000120) - it('does not create a timer on subsequent requests', async () => { - await appMetrics.queueMetric({ ...metric, successes: 1 }, timestamp) - const originalTimer = appMetrics.timer await appMetrics.queueMetric({ ...metric, successes: 1 }, timestamp) - expect(originalTimer).not.toBeNull() - expect(appMetrics.timer).toEqual(originalTimer) - }) - - it('does nothing if feature is not available', async () => { - jest.mocked(hub.organizationManager.hasAvailableFeature).mockResolvedValue(false) - + expect(appMetrics.flush).toHaveBeenCalledTimes(1) + // doesn't flush again on the next call, i.e. flust metrics were reset + Date.now = jest.fn(() => 1600000130) await appMetrics.queueMetric({ ...metric, successes: 1 }, timestamp) - expect(appMetrics.queuedData).toEqual({}) + expect(appMetrics.flush).toHaveBeenCalledTimes(1) }) - it('does not query `hasAvailableFeature` if not needed', async () => { - hub.APP_METRICS_GATHERED_FOR_ALL = true - - await appMetrics.queueMetric({ ...metric, successes: 1 }, timestamp) - - expect(appMetrics.queuedData).not.toEqual({}) - expect(hub.organizationManager.hasAvailableFeature).not.toHaveBeenCalled() + it('flushes when max queue size is hit', async () => { + jest.spyOn(appMetrics, 'flush') + // parallel could trigger multiple flushes and make the test flaky + for (let i = 0; i < 7; i++) { + await appMetrics.queueMetric({ ...metric, successes: 1, teamId: i }, timestamp) + } + expect(appMetrics.flush).toHaveBeenCalledTimes(1) + // we only count different keys, so this should not trigger a flush + for (let i = 0; i < 7; i++) { + await appMetrics.queueMetric({ ...metric, successes: 1 }, timestamp) + } + expect(appMetrics.flush).toHaveBeenCalledTimes(1) }) }) From b4e5ac6be4e1944b6c5e172f396949c850faa7f0 Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Mon, 25 Sep 2023 15:47:20 +0100 Subject: [PATCH 11/22] feat(surveys): Add opt in migration (#17600) --- latest_migrations.manifest | 2 +- .../api/test/__snapshots__/test_action.ambr | 3 +++ .../test/__snapshots__/test_annotation.ambr | 3 +++ .../api/test/__snapshots__/test_decide.ambr | 4 ++++ .../test_early_access_feature.ambr | 2 ++ .../api/test/__snapshots__/test_element.ambr | 1 + .../api/test/__snapshots__/test_insight.ambr | 11 +++++++++++ .../api/test/__snapshots__/test_preflight.ambr | 1 + .../api/test/__snapshots__/test_survey.ambr | 1 + posthog/migrations/0351_team_surveys_opt_in.py | 18 ++++++++++++++++++ posthog/models/team/team.py | 1 + .../test/__snapshots__/test_feature_flag.ambr | 1 + 12 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 posthog/migrations/0351_team_surveys_opt_in.py diff --git a/latest_migrations.manifest b/latest_migrations.manifest index 233b3d446d5cb..0f95d248d4675 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: 0350_add_notebook_text_content +posthog: 0351_team_surveys_opt_in sessions: 0001_initial social_django: 0010_uid_db_index two_factor: 0007_auto_20201201_1019 diff --git a/posthog/api/test/__snapshots__/test_action.ambr b/posthog/api/test/__snapshots__/test_action.ambr index 7bdd2eaad7cfb..e158a0baaf2da 100644 --- a/posthog/api/test/__snapshots__/test_action.ambr +++ b/posthog/api/test/__snapshots__/test_action.ambr @@ -48,6 +48,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", @@ -197,6 +198,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", @@ -513,6 +515,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", diff --git a/posthog/api/test/__snapshots__/test_annotation.ambr b/posthog/api/test/__snapshots__/test_annotation.ambr index eceea7ffb0ead..23060eec170ac 100644 --- a/posthog/api/test/__snapshots__/test_annotation.ambr +++ b/posthog/api/test/__snapshots__/test_annotation.ambr @@ -48,6 +48,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", @@ -121,6 +122,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", @@ -437,6 +439,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", diff --git a/posthog/api/test/__snapshots__/test_decide.ambr b/posthog/api/test/__snapshots__/test_decide.ambr index ae9901067c7d0..43b1197657722 100644 --- a/posthog/api/test/__snapshots__/test_decide.ambr +++ b/posthog/api/test/__snapshots__/test_decide.ambr @@ -48,6 +48,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", @@ -282,6 +283,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", @@ -431,6 +433,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", @@ -575,6 +578,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", diff --git a/posthog/api/test/__snapshots__/test_early_access_feature.ambr b/posthog/api/test/__snapshots__/test_early_access_feature.ambr index f5b0252c90b0e..f9cfe83157dc4 100644 --- a/posthog/api/test/__snapshots__/test_early_access_feature.ambr +++ b/posthog/api/test/__snapshots__/test_early_access_feature.ambr @@ -19,6 +19,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", @@ -145,6 +146,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", diff --git a/posthog/api/test/__snapshots__/test_element.ambr b/posthog/api/test/__snapshots__/test_element.ambr index 943571f635436..8b1ea3b9ebfc4 100644 --- a/posthog/api/test/__snapshots__/test_element.ambr +++ b/posthog/api/test/__snapshots__/test_element.ambr @@ -48,6 +48,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", diff --git a/posthog/api/test/__snapshots__/test_insight.ambr b/posthog/api/test/__snapshots__/test_insight.ambr index 0e921f83f3f37..f9015d5d9b82c 100644 --- a/posthog/api/test/__snapshots__/test_insight.ambr +++ b/posthog/api/test/__snapshots__/test_insight.ambr @@ -646,6 +646,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", @@ -690,6 +691,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", @@ -810,6 +812,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", @@ -1032,6 +1035,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", @@ -1166,6 +1170,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", @@ -1287,6 +1292,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", @@ -1388,6 +1394,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", @@ -1524,6 +1531,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", @@ -1603,6 +1611,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", @@ -1681,6 +1690,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", @@ -1732,6 +1742,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", diff --git a/posthog/api/test/__snapshots__/test_preflight.ambr b/posthog/api/test/__snapshots__/test_preflight.ambr index 19a504d23eb28..6981b2f9afe16 100644 --- a/posthog/api/test/__snapshots__/test_preflight.ambr +++ b/posthog/api/test/__snapshots__/test_preflight.ambr @@ -59,6 +59,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", diff --git a/posthog/api/test/__snapshots__/test_survey.ambr b/posthog/api/test/__snapshots__/test_survey.ambr index f076ff00feeda..dacdb0ebc8550 100644 --- a/posthog/api/test/__snapshots__/test_survey.ambr +++ b/posthog/api/test/__snapshots__/test_survey.ambr @@ -94,6 +94,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", diff --git a/posthog/migrations/0351_team_surveys_opt_in.py b/posthog/migrations/0351_team_surveys_opt_in.py new file mode 100644 index 0000000000000..c1722b7a11000 --- /dev/null +++ b/posthog/migrations/0351_team_surveys_opt_in.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.19 on 2023-09-20 14:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("posthog", "0350_add_notebook_text_content"), + ] + + operations = [ + migrations.AddField( + model_name="team", + name="surveys_opt_in", + field=models.BooleanField(blank=True, null=True), + ), + ] diff --git a/posthog/models/team/team.py b/posthog/models/team/team.py index 5b3ceb039519f..73f1231d33bb0 100644 --- a/posthog/models/team/team.py +++ b/posthog/models/team/team.py @@ -150,6 +150,7 @@ class Team(UUIDClassicModel): session_recording_opt_in: models.BooleanField = models.BooleanField(default=False) capture_console_log_opt_in: models.BooleanField = models.BooleanField(null=True, blank=True) capture_performance_opt_in: models.BooleanField = models.BooleanField(null=True, blank=True) + surveys_opt_in: models.BooleanField = models.BooleanField(null=True, blank=True) session_recording_version: models.CharField = models.CharField(null=True, blank=True, max_length=24) signup_token: models.CharField = models.CharField(max_length=200, null=True, blank=True) is_demo: models.BooleanField = models.BooleanField(default=False) diff --git a/posthog/test/__snapshots__/test_feature_flag.ambr b/posthog/test/__snapshots__/test_feature_flag.ambr index d9ed15e4f2cdc..84422798972fb 100644 --- a/posthog/test/__snapshots__/test_feature_flag.ambr +++ b/posthog/test/__snapshots__/test_feature_flag.ambr @@ -19,6 +19,7 @@ "posthog_team"."session_recording_opt_in", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", "posthog_team"."session_recording_version", "posthog_team"."signup_token", "posthog_team"."is_demo", From c16fb28dbe2d51693aa194ae2b69d51fd39ce6ab Mon Sep 17 00:00:00 2001 From: PostHog Bot <69588470+posthog-bot@users.noreply.github.com> Date: Mon, 25 Sep 2023 11:03:46 -0400 Subject: [PATCH 12/22] chore(deps): Update posthog-js to 1.80.0 (#17603) --- package.json | 2 +- pnpm-lock.yaml | 158 ++++++++++++++++++++++++++++++++----------------- 2 files changed, 105 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index 9e0ac7481ede1..0aaa5a82c9b1f 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "md5": "^2.3.0", "monaco-editor": "^0.39.0", "papaparse": "^5.4.1", - "posthog-js": "1.79.1", + "posthog-js": "1.80.0", "posthog-js-lite": "2.0.0-alpha5", "prettier": "^2.8.8", "prop-types": "^15.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8bfe1d9d3974..78ba5bff58496 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -198,8 +198,8 @@ dependencies: specifier: ^5.4.1 version: 5.4.1 posthog-js: - specifier: 1.79.1 - version: 1.79.1 + specifier: 1.80.0 + version: 1.80.0 posthog-js-lite: specifier: 2.0.0-alpha5 version: 2.0.0-alpha5 @@ -617,7 +617,7 @@ devDependencies: version: 7.3.1 storybook-addon-pseudo-states: specifier: 2.1.0 - version: 2.1.0(@storybook/components@7.3.1)(@storybook/core-events@7.3.1)(@storybook/manager-api@7.4.3)(@storybook/preview-api@7.4.3)(@storybook/theming@7.3.1)(react-dom@16.14.0)(react@16.14.0) + version: 2.1.0(@storybook/components@7.3.1)(@storybook/core-events@7.3.1)(@storybook/manager-api@7.4.5)(@storybook/preview-api@7.4.5)(@storybook/theming@7.3.1)(react-dom@16.14.0)(react@16.14.0) style-loader: specifier: ^2.0.0 version: 2.0.0(webpack@5.88.2) @@ -982,12 +982,12 @@ packages: dependencies: '@babel/types': 7.22.10 - /@babel/parser@7.22.16: - resolution: {integrity: sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==} + /@babel/parser@7.23.0: + resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.22.19 + '@babel/types': 7.23.0 dev: true /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.5(@babel/core@7.22.10): @@ -2092,8 +2092,8 @@ packages: '@babel/helper-validator-identifier': 7.22.5 to-fast-properties: 2.0.0 - /@babel/types@7.22.19: - resolution: {integrity: sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==} + /@babel/types@7.23.0: + resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==} engines: {node: '>=6.9.0'} dependencies: '@babel/helper-string-parser': 7.22.5 @@ -4532,11 +4532,11 @@ packages: tiny-invariant: 1.3.1 dev: true - /@storybook/channels@7.4.3: - resolution: {integrity: sha512-lIoRX3EV0wKPX8ojIrJUtsOv4+Gv8r9pfJpam/NdyYd+rs0AjDK13ieINRfBMnJkfjsWa3vmZtGMBEVvDKwTMw==} + /@storybook/channels@7.4.5: + resolution: {integrity: sha512-zWPZn4CxPFXsrrSRQ9JD8GmTeWeFYgr3sTBpe23hnhYookCXVNJ6AcaXogrT9b2ALfbB6MiFDbZIHHTgIgbWpg==} dependencies: - '@storybook/client-logger': 7.4.3 - '@storybook/core-events': 7.4.3 + '@storybook/client-logger': 7.4.5 + '@storybook/core-events': 7.4.5 '@storybook/global': 5.0.0 qs: 6.11.2 telejson: 7.2.0 @@ -4600,8 +4600,8 @@ packages: '@storybook/global': 5.0.0 dev: true - /@storybook/client-logger@7.4.3: - resolution: {integrity: sha512-Nhngo9X4HjN00aRhgIVGWbwkWPe0Fz8PySuxnd8nAxSsz7KpdLFyYo2TbZZ3TX51FG5Fxcb0G5OHuunItP7EWQ==} + /@storybook/client-logger@7.4.5: + resolution: {integrity: sha512-Bn6eTAjhPDUfLpvuxhKkpDpOtkadfkSmkBNBZRu3r0Dzk2J1nNyKV5K6D8dOU4PFVof4z/gXYj5bktT29jKsmw==} dependencies: '@storybook/global': 5.0.0 dev: true @@ -4692,8 +4692,8 @@ packages: resolution: {integrity: sha512-7Pkgwmj/9B7Z3NNSn2swnviBrg9L1VeYSFw6JJKxtQskt8QoY8LxAsPzVMlHjqRmO6sO7lHo9FgpzIFxdmFaAA==} dev: true - /@storybook/core-events@7.4.3: - resolution: {integrity: sha512-FRfipCijMnVbGxL1ZjOLM836lyd/TGQcUFeVjTQWW/+pIGHELqDHiYeq68hqoGTKl0G0np59CJPWYTUZA4Dl9Q==} + /@storybook/core-events@7.4.5: + resolution: {integrity: sha512-Jzy/adSC95saYCZlgXE5j7jmiMLAXYpnBFBxEtBdXwSWEBb0zt21n1nyWBEAv9s/k2gqDXlPHKHeL5Mn6y40zA==} dependencies: ts-dedent: 2.2.0 dev: true @@ -4858,20 +4858,20 @@ packages: ts-dedent: 2.2.0 dev: true - /@storybook/manager-api@7.4.3(react-dom@16.14.0)(react@16.14.0): - resolution: {integrity: sha512-o5oiL2cJKlY+HNBCdUo5QKT8yXTyYYvBKibSS3YfDKcjeR9RXP+RhdF5lLLh6TzPwfdtLrXQoVI4A/61v2kurQ==} + /@storybook/manager-api@7.4.5(react-dom@16.14.0)(react@16.14.0): + resolution: {integrity: sha512-8Hdh5Tutet8xRy2fAknczfvpshz09eVnLd8m34vcFceUOYvEnvDbWerufhlEzovsF4v7U32uqbDHKdKTamWEQQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/channels': 7.4.3 - '@storybook/client-logger': 7.4.3 - '@storybook/core-events': 7.4.3 + '@storybook/channels': 7.4.5 + '@storybook/client-logger': 7.4.5 + '@storybook/core-events': 7.4.5 '@storybook/csf': 0.1.1 '@storybook/global': 5.0.0 - '@storybook/router': 7.4.3(react-dom@16.14.0)(react@16.14.0) - '@storybook/theming': 7.4.3(react-dom@16.14.0)(react@16.14.0) - '@storybook/types': 7.4.3 + '@storybook/router': 7.4.5(react-dom@16.14.0)(react@16.14.0) + '@storybook/theming': 7.4.5(react-dom@16.14.0)(react@16.14.0) + '@storybook/types': 7.4.5 dequal: 2.0.3 lodash: 4.17.21 memoizerific: 1.11.3 @@ -4967,15 +4967,15 @@ packages: util-deprecate: 1.0.2 dev: true - /@storybook/preview-api@7.4.3: - resolution: {integrity: sha512-qKwfH2+qN1Zpz2UX6dQLiTU5x2JH3o/+jOY4GYF6c3atTm5WAu1OvCYAJVb6MdXfAhZNuPwDKnJR8VmzWplWBg==} + /@storybook/preview-api@7.4.5: + resolution: {integrity: sha512-6xXQZPyilkGVddfZBI7tMbMMgOyIoZTYgTnwSPTMsXxO0f0TvtNDmGdwhn0I1nREHKfiQGpcQe6gwddEMnGtSg==} dependencies: - '@storybook/channels': 7.4.3 - '@storybook/client-logger': 7.4.3 - '@storybook/core-events': 7.4.3 + '@storybook/channels': 7.4.5 + '@storybook/client-logger': 7.4.5 + '@storybook/core-events': 7.4.5 '@storybook/csf': 0.1.1 '@storybook/global': 5.0.0 - '@storybook/types': 7.4.3 + '@storybook/types': 7.4.5 '@types/qs': 6.9.8 dequal: 2.0.3 lodash: 4.17.21 @@ -5110,13 +5110,13 @@ packages: react-dom: 16.14.0(react@16.14.0) dev: true - /@storybook/router@7.4.3(react-dom@16.14.0)(react@16.14.0): - resolution: {integrity: sha512-1ab1VTYzzOsBGKeT8xm1kLriIsIsiB/l3t7DdARJxLmPbddKyyXE018w17gfrARCWQ8SM99Ko6+pLmlZ2sm8ug==} + /@storybook/router@7.4.5(react-dom@16.14.0)(react@16.14.0): + resolution: {integrity: sha512-IM4IhiPiXsx3FAUeUOAB47uiuUS8Yd37VQcNlXLBO28GgHoTSYOrjS+VTGLIV5cAGKr8+H5pFB+q35BnlFUpkQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/client-logger': 7.4.3 + '@storybook/client-logger': 7.4.5 memoizerific: 1.11.3 qs: 6.11.2 react: 16.14.0 @@ -5209,14 +5209,14 @@ packages: react-dom: 16.14.0(react@16.14.0) dev: true - /@storybook/theming@7.4.3(react-dom@16.14.0)(react@16.14.0): - resolution: {integrity: sha512-u5wLwWmhGcTmkcs6f2wDGv+w8wzwbNJat0WaIIbwdJfX7arH6nO5HkBhNxvl6FUFxX0tovp/e9ULzxVPc356jw==} + /@storybook/theming@7.4.5(react-dom@16.14.0)(react@16.14.0): + resolution: {integrity: sha512-QSIJDIMzOegzlhubIBaYIovf4mlf+AVL0SmQOskPS8GZ6s9t77yUUI6gZTEjO+S4eB3djXRsfTTijQ8+z4XmRA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@16.14.0) - '@storybook/client-logger': 7.4.3 + '@storybook/client-logger': 7.4.5 '@storybook/global': 5.0.0 memoizerific: 1.11.3 react: 16.14.0 @@ -5232,12 +5232,12 @@ packages: file-system-cache: 2.3.0 dev: true - /@storybook/types@7.4.3: - resolution: {integrity: sha512-DrHC1hIiw9TqDILLokDnvbUPNxGz5iJaYFEv30uvYE0s9MvgEUPblCChEUjaHOps7zQTznMPf8ULfoXlgqxk2A==} + /@storybook/types@7.4.5: + resolution: {integrity: sha512-DTWFNjfRTpncjufDoUs0QnNkgHG2qThGKWL1D6sO18cYI02zWPyHWD8/cbqlvtT7XIGe3s1iUEfCTdU5GcwWBA==} dependencies: - '@storybook/channels': 7.4.3 + '@storybook/channels': 7.4.5 '@types/babel__core': 7.20.2 - '@types/express': 4.17.17 + '@types/express': 4.17.18 file-system-cache: 2.3.0 dev: true @@ -5732,8 +5732,8 @@ packages: /@types/babel__core@7.20.2: resolution: {integrity: sha512-pNpr1T1xLUc2l3xJKuPtsEky3ybxN3m4fJkknfIpTCTfIZCDW57oAg+EfCgIIp2rvCe0Wn++/FfodDS4YXxBwA==} dependencies: - '@babel/parser': 7.22.16 - '@babel/types': 7.22.19 + '@babel/parser': 7.23.0 + '@babel/types': 7.23.0 '@types/babel__generator': 7.6.5 '@types/babel__template': 7.4.2 '@types/babel__traverse': 7.20.2 @@ -5748,7 +5748,7 @@ packages: /@types/babel__generator@7.6.5: resolution: {integrity: sha512-h9yIuWbJKdOPLJTbmSpPzkF67e659PbQDba7ifWm5BJ8xTv+sDmS7rFmywkWOvXedGTivCdeGSIIX8WLcRTz8w==} dependencies: - '@babel/types': 7.22.19 + '@babel/types': 7.23.0 dev: true /@types/babel__template@7.4.1: @@ -5761,8 +5761,8 @@ packages: /@types/babel__template@7.4.2: resolution: {integrity: sha512-/AVzPICMhMOMYoSx9MoKpGDKdBRsIXMNByh1PXSZoa+v6ZoLa8xxtsT/uLQ/NJm0XVAWl/BvId4MlDeXJaeIZQ==} dependencies: - '@babel/parser': 7.22.16 - '@babel/types': 7.22.19 + '@babel/parser': 7.23.0 + '@babel/types': 7.23.0 dev: true /@types/babel__traverse@7.18.2: @@ -5774,7 +5774,7 @@ packages: /@types/babel__traverse@7.20.2: resolution: {integrity: sha512-ojlGK1Hsfce93J0+kn3H5R73elidKUaZonirN33GSmgTUMpzI/MIFfSpF3haANe3G1bEBS9/9/QEqwTzwqFsKw==} dependencies: - '@babel/types': 7.22.19 + '@babel/types': 7.23.0 dev: true /@types/body-parser@1.19.2: @@ -5784,6 +5784,13 @@ packages: '@types/node': 18.11.9 dev: true + /@types/body-parser@1.19.3: + resolution: {integrity: sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==} + dependencies: + '@types/connect': 3.4.36 + '@types/node': 18.11.9 + dev: true + /@types/chart.js@2.9.37: resolution: {integrity: sha512-9bosRfHhkXxKYfrw94EmyDQcdjMaQPkU1fH2tDxu8DWXxf1mjzWQAV4laJF51ZbC2ycYwNDvIm1rGez8Bug0vg==} dependencies: @@ -5806,6 +5813,12 @@ packages: '@types/node': 18.11.9 dev: true + /@types/connect@3.4.36: + resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} + dependencies: + '@types/node': 18.11.9 + dev: true + /@types/cookie@0.4.1: resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} dev: true @@ -6072,6 +6085,15 @@ packages: '@types/send': 0.17.1 dev: true + /@types/express-serve-static-core@4.17.37: + resolution: {integrity: sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==} + dependencies: + '@types/node': 18.11.9 + '@types/qs': 6.9.8 + '@types/range-parser': 1.2.4 + '@types/send': 0.17.2 + dev: true + /@types/express@4.17.17: resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} dependencies: @@ -6081,6 +6103,15 @@ packages: '@types/serve-static': 1.15.2 dev: true + /@types/express@4.17.18: + resolution: {integrity: sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==} + dependencies: + '@types/body-parser': 1.19.3 + '@types/express-serve-static-core': 4.17.37 + '@types/qs': 6.9.8 + '@types/serve-static': 1.15.3 + dev: true + /@types/find-cache-dir@3.2.1: resolution: {integrity: sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==} dev: true @@ -6108,6 +6139,10 @@ packages: resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==} dev: true + /@types/http-errors@2.0.2: + resolution: {integrity: sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==} + dev: true + /@types/image-blob-reduce@4.1.1: resolution: {integrity: sha512-Oe2EPjW+iZSsXccxZPebqHqXAUaOLir3eQVqPx0ryXeJZdCZx+gYvWBZtqYEcluP6f3bll1m06ahT26bX0+LOg==} dependencies: @@ -6377,6 +6412,13 @@ packages: '@types/node': 18.11.9 dev: true + /@types/send@0.17.2: + resolution: {integrity: sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==} + dependencies: + '@types/mime': 1.3.2 + '@types/node': 18.11.9 + dev: true + /@types/serve-static@1.15.2: resolution: {integrity: sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==} dependencies: @@ -6385,6 +6427,14 @@ packages: '@types/node': 18.11.9 dev: true + /@types/serve-static@1.15.3: + resolution: {integrity: sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==} + dependencies: + '@types/http-errors': 2.0.2 + '@types/mime': 3.0.1 + '@types/node': 18.11.9 + dev: true + /@types/set-cookie-parser@2.4.2: resolution: {integrity: sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==} dependencies: @@ -6442,8 +6492,8 @@ packages: '@types/yargs-parser': 21.0.0 dev: true - /@types/yauzl@2.10.0: - resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} + /@types/yauzl@2.10.1: + resolution: {integrity: sha512-CHzgNU3qYBnp/O4S3yv2tXPlvMTq0YWSTVg2/JYLqWZGHwwgJGAwd00poay/11asPq8wLFwHzubyInqHIFmmiw==} requiresBuild: true dependencies: '@types/node': 18.11.9 @@ -10289,7 +10339,7 @@ packages: get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: - '@types/yauzl': 2.10.0 + '@types/yauzl': 2.10.1 transitivePeerDependencies: - supports-color dev: true @@ -14966,8 +15016,8 @@ packages: resolution: {integrity: sha512-tlkBdypJuvK/s00n4EiQjwYVfuuZv6vt8BF3g1ooIQa2Gz9Vz80p8q3qsPLZ0V5ErGRy6i3Q4fWC9TDzR7GNRQ==} dev: false - /posthog-js@1.79.1: - resolution: {integrity: sha512-ftW9RHoB9gIYjqVcA/YJeu99MfJaX/vfx/ADgO2yi5QfFWsIWNnfPeWYQskMMxEUTq03svRAwdZHTyOkVkDpIA==} + /posthog-js@1.80.0: + resolution: {integrity: sha512-GAbdSqNG1fsXqdmG2Wx9nuZbK/LlpDUGUC+OQyFWNRylGAczSc8TIEErupQYxDmybxtC7f2/1Jtw/fgyVNLnRA==} dependencies: fflate: 0.4.8 dev: false @@ -17192,7 +17242,7 @@ packages: resolution: {integrity: sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==} dev: true - /storybook-addon-pseudo-states@2.1.0(@storybook/components@7.3.1)(@storybook/core-events@7.3.1)(@storybook/manager-api@7.4.3)(@storybook/preview-api@7.4.3)(@storybook/theming@7.3.1)(react-dom@16.14.0)(react@16.14.0): + /storybook-addon-pseudo-states@2.1.0(@storybook/components@7.3.1)(@storybook/core-events@7.3.1)(@storybook/manager-api@7.4.5)(@storybook/preview-api@7.4.5)(@storybook/theming@7.3.1)(react-dom@16.14.0)(react@16.14.0): resolution: {integrity: sha512-AwbCL1OiZ16aIeXSP/IOovkMwXy7NTZqmjkz+UM2guSGjvogHNA95NhuVyWoqieE+QWUpGO48+MrBGMeeJcHOQ==} peerDependencies: '@storybook/components': ^7.0.0 @@ -17210,8 +17260,8 @@ packages: dependencies: '@storybook/components': 7.3.1(@types/react-dom@16.9.17)(@types/react@16.14.34)(react-dom@16.14.0)(react@16.14.0) '@storybook/core-events': 7.3.1 - '@storybook/manager-api': 7.4.3(react-dom@16.14.0)(react@16.14.0) - '@storybook/preview-api': 7.4.3 + '@storybook/manager-api': 7.4.5(react-dom@16.14.0)(react@16.14.0) + '@storybook/preview-api': 7.4.5 '@storybook/theming': 7.3.1(react-dom@16.14.0)(react@16.14.0) react: 16.14.0 react-dom: 16.14.0(react@16.14.0) From 7a8a268790fbfc9c91d300ad8d93114a55e76601 Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Mon, 25 Sep 2023 08:15:55 -0700 Subject: [PATCH 13/22] feat: new onboarding navigation (#17582) * kinda make the url navigation work * make it work for unnamed steps, just use number * handle unnamed steps navigation * include search params in redirect path so can get back to proper step * explain things * more explaining --- frontend/src/scenes/billing/billingLogic.ts | 2 +- .../src/scenes/onboarding/onboardingLogic.tsx | 102 +++++++++++++++++- 2 files changed, 99 insertions(+), 5 deletions(-) diff --git a/frontend/src/scenes/billing/billingLogic.ts b/frontend/src/scenes/billing/billingLogic.ts index 2fdc2aa0f56ee..9a0d1eff86ec7 100644 --- a/frontend/src/scenes/billing/billingLogic.ts +++ b/frontend/src/scenes/billing/billingLogic.ts @@ -77,7 +77,7 @@ export const billingLogic = kea([ return window.location.pathname.includes('/ingestion') ? urls.ingestion() + '/billing' : window.location.pathname.includes('/onboarding') - ? window.location.pathname + ? window.location.pathname + window.location.search : '' }, }, diff --git a/frontend/src/scenes/onboarding/onboardingLogic.tsx b/frontend/src/scenes/onboarding/onboardingLogic.tsx index 104779a1f4da4..c896cff3f396e 100644 --- a/frontend/src/scenes/onboarding/onboardingLogic.tsx +++ b/frontend/src/scenes/onboarding/onboardingLogic.tsx @@ -8,6 +8,23 @@ import { billingLogic } from 'scenes/billing/billingLogic' export interface OnboardingLogicProps { productKey: ProductKey | null } + +export enum OnboardingStepKey { + PRODUCT_INTRO = 'product_intro', + SDKS = 'sdks', + BILLING = 'billing', + PAIRS_WITH = 'pairs_with', +} + +export type OnboardingStepMap = Record + +const onboardingStepMap: OnboardingStepMap = { + [OnboardingStepKey.PRODUCT_INTRO]: 'OnboardingProductIntro', + [OnboardingStepKey.SDKS]: 'SDKs', + [OnboardingStepKey.BILLING]: 'OnboardingBillingStep', + [OnboardingStepKey.PAIRS_WITH]: 'OnboardingPairsWithStep', +} + export type AllOnboardingSteps = JSX.Element[] export const onboardingLogic = kea({ @@ -51,6 +68,12 @@ export const onboardingLogic = kea({ setAllOnboardingSteps: (_, { allOnboardingSteps }) => allOnboardingSteps as AllOnboardingSteps, }, ], + stepKey: [ + '' as string, + { + setStepKey: (_, { stepKey }) => stepKey, + }, + ], onCompleteOnbardingRedirectUrl: [ urls.default() as string, { @@ -110,9 +133,74 @@ export const onboardingLogic = kea({ completeOnboarding: () => { window.location.href = values.onCompleteOnbardingRedirectUrl }, + setAllOnboardingSteps: ({ allOnboardingSteps }) => { + // once we have the onboarding steps we need to make sure the step key is valid, + // and if so use it to set the step number. if not valid, remove it from the state. + // valid step keys are either numbers (used for unnamed steps) or keys from the onboardingStepMap. + // if it's a number, we try to convert it to a named step key using the onboardingStepMap. + let stepKey = values.stepKey + if (values.stepKey) { + if (parseInt(values.stepKey) > 0) { + // try to convert the step number to a step key + const stepName = allOnboardingSteps[parseInt(values.stepKey) - 1]?.type?.name + const newStepKey = Object.keys(onboardingStepMap).find((key) => onboardingStepMap[key] === stepName) + if (stepName && stepKey) { + stepKey = newStepKey || stepKey + actions.setStepKey(stepKey) + } + } + if (stepKey in onboardingStepMap) { + const stepIndex = allOnboardingSteps + .map((step) => step.type.name) + .indexOf(onboardingStepMap[stepKey as OnboardingStepKey]) + if (stepIndex > -1) { + actions.setCurrentOnboardingStepNumber(stepIndex + 1) + } else { + actions.setStepKey('') + actions.setCurrentOnboardingStepNumber(1) + } + } else if ( + // if it's a number, just use that and set the correct onboarding step number + parseInt(stepKey) > 1 && + allOnboardingSteps.length > 0 && + allOnboardingSteps[parseInt(stepKey) - 1] + ) { + actions.setCurrentOnboardingStepNumber(parseInt(stepKey)) + } + } + }, + setStepKey: ({ stepKey }) => { + // if the step key is invalid (doesn't exist in the onboardingStepMap or the allOnboardingSteps array) + // remove it from the state. Numeric step keys are also allowed, as long as they are a valid + // index for the allOnboardingSteps array. + if ( + stepKey && + values.allOnboardingSteps.length > 0 && + (!values.allOnboardingSteps.find( + (step) => step.type.name === onboardingStepMap[stepKey as OnboardingStepKey] + ) || + !values.allOnboardingSteps[parseInt(stepKey) - 1]) + ) { + actions.setStepKey('') + } + }, }), - urlToAction: ({ actions }) => ({ - '/onboarding/:productKey': ({ productKey }, { success, upgraded }) => { + actionToUrl: ({ values }) => ({ + setCurrentOnboardingStepNumber: () => { + // when the current step number changes, update the url to reflect the new step + const stepName = values.allOnboardingSteps[values.currentOnboardingStepNumber - 1]?.type?.name + const stepKey = + Object.keys(onboardingStepMap).find((key) => onboardingStepMap[key] === stepName) || + values.currentOnboardingStepNumber.toString() + if (stepKey) { + return [`/onboarding/${values.productKey}`, { step: stepKey }] + } else { + return [`/onboarding/${values.productKey}`] + } + }, + }), + urlToAction: ({ actions, values }) => ({ + '/onboarding/:productKey': ({ productKey }, { success, upgraded, step }) => { if (!productKey) { window.location.href = urls.default() return @@ -120,8 +208,14 @@ export const onboardingLogic = kea({ if (success || upgraded) { actions.setSubscribedDuringOnboarding(true) } - actions.setProductKey(productKey) - actions.setCurrentOnboardingStepNumber(1) + if (productKey !== values.productKey) { + actions.setProductKey(productKey) + } + if (step && (step in onboardingStepMap || parseInt(step) > 0)) { + actions.setStepKey(step) + } else { + actions.setCurrentOnboardingStepNumber(1) + } }, }), }) From 00e4477a5a5da6772044d632bb5f6217368e4c0b Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Mon, 25 Sep 2023 16:22:49 -0700 Subject: [PATCH 14/22] feat: onboarding invite members, other products, verification steps (#17615) * invite member button on sdks * add other products step * fix * update the team on complete * show first/next properly * fix * update language * add onstart func to do things when onboarding started * put suggestedProducts in logic * fix * add verification step * add feature flags SDKs section (but SDKs are incorrect!) * let people skip picking a product * Update UI snapshots for `chromium` (1) --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- frontend/src/scenes/onboarding/Onboarding.tsx | 30 +++++- .../onboarding/OnboardingBillingStep.tsx | 2 +- .../OnboardingOtherProductsStep.tsx | 54 ++++++++++ .../onboarding/OnboardingProductIntro.tsx | 13 ++- .../src/scenes/onboarding/OnboardingStep.tsx | 12 ++- .../onboarding/OnboardingVerificationStep.tsx | 51 ++++++++++ .../src/scenes/onboarding/onboardingLogic.tsx | 39 ++++++-- frontend/src/scenes/onboarding/sdks/SDKs.tsx | 10 +- .../FeatureFlagsSDKInstructions.tsx | 8 ++ .../onboarding/sdks/feature-flags/index.tsx | 3 + .../onboarding/sdks/feature-flags/js-web.tsx | 42 ++++++++ .../onboarding/sdks/feature-flags/next-js.tsx | 98 +++++++++++++++++++ .../onboarding/sdks/feature-flags/react.tsx | 64 ++++++++++++ frontend/src/scenes/products/Products.tsx | 28 ++++-- 14 files changed, 423 insertions(+), 31 deletions(-) create mode 100644 frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx create mode 100644 frontend/src/scenes/onboarding/OnboardingVerificationStep.tsx create mode 100644 frontend/src/scenes/onboarding/sdks/feature-flags/FeatureFlagsSDKInstructions.tsx create mode 100644 frontend/src/scenes/onboarding/sdks/feature-flags/index.tsx create mode 100644 frontend/src/scenes/onboarding/sdks/feature-flags/js-web.tsx create mode 100644 frontend/src/scenes/onboarding/sdks/feature-flags/next-js.tsx create mode 100644 frontend/src/scenes/onboarding/sdks/feature-flags/react.tsx diff --git a/frontend/src/scenes/onboarding/Onboarding.tsx b/frontend/src/scenes/onboarding/Onboarding.tsx index 41b299c417f36..3cf1c4989e4c1 100644 --- a/frontend/src/scenes/onboarding/Onboarding.tsx +++ b/frontend/src/scenes/onboarding/Onboarding.tsx @@ -11,6 +11,10 @@ import { ProductKey } from '~/types' import { ProductAnalyticsSDKInstructions } from './sdks/product-analytics/ProductAnalyticsSDKInstructions' import { SessionReplaySDKInstructions } from './sdks/session-replay/SessionReplaySDKInstructions' import { OnboardingBillingStep } from './OnboardingBillingStep' +import { OnboardingOtherProductsStep } from './OnboardingOtherProductsStep' +import { teamLogic } from 'scenes/teamLogic' +import { OnboardingVerificationStep } from './OnboardingVerificationStep' +import { FeatureFlagsSDKInstructions } from './sdks/feature-flags/FeatureFlagsSDKInstructions' export const scene: SceneExport = { component: Onboarding, @@ -20,7 +24,7 @@ export const scene: SceneExport = { /** * Wrapper for custom onboarding content. This automatically includes the product intro and billing step. */ -const OnboardingWrapper = ({ children }: { children: React.ReactNode }): JSX.Element => { +const OnboardingWrapper = ({ children, onStart }: { children: React.ReactNode; onStart?: () => void }): JSX.Element => { const { currentOnboardingStepNumber, shouldShowBillingStep } = useValues(onboardingLogic) const { setAllOnboardingSteps } = useActions(onboardingLogic) const { product } = useValues(onboardingLogic) @@ -42,7 +46,8 @@ const OnboardingWrapper = ({ children }: { children: React.ReactNode }): JSX.Ele } const createAllSteps = (): void => { - const ProductIntro = + const ProductIntro = + const OtherProductsStep = let steps = [] if (Array.isArray(children)) { steps = [ProductIntro, ...children] @@ -53,6 +58,7 @@ const OnboardingWrapper = ({ children }: { children: React.ReactNode }): JSX.Ele const BillingStep = steps = [...steps, BillingStep] } + steps = [...steps, OtherProductsStep] setAllSteps(steps) } @@ -63,22 +69,36 @@ const ProductAnalyticsOnboarding = (): JSX.Element => { return ( + ) } const SessionReplayOnboarding = (): JSX.Element => { + const { updateCurrentTeam } = useActions(teamLogic) return ( - + { + updateCurrentTeam({ + session_recording_opt_in: true, + capture_console_log_opt_in: true, + capture_performance_opt_in: true, + }) + }} + > ) } const FeatureFlagsOnboarding = (): JSX.Element => { - return {/* */} + return ( + + + + ) } export function Onboarding(): JSX.Element | null { diff --git a/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx b/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx index 510f91ebcdf8e..6daba12e33fe6 100644 --- a/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx @@ -50,7 +50,7 @@ export const OnboardingBillingStep = ({ product }: { product: BillingProductV2Ty

Subscribe successful

-

You're all ready to use PostHog.

+

You're all ready to use {product.name}.

diff --git a/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx b/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx new file mode 100644 index 0000000000000..56102c51a7646 --- /dev/null +++ b/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx @@ -0,0 +1,54 @@ +import { LemonButton, LemonCard } from '@posthog/lemon-ui' +import { OnboardingStep } from './OnboardingStep' +import { onboardingLogic } from './onboardingLogic' +import { useActions, useValues } from 'kea' +import { urls } from 'scenes/urls' + +export const OnboardingOtherProductsStep = (): JSX.Element => { + const { product, suggestedProducts } = useValues(onboardingLogic) + const { completeOnboarding } = useActions(onboardingLogic) + if (suggestedProducts.length === 0) { + completeOnboarding() + } + + return ( + } + > +
+ {suggestedProducts?.map((suggestedProduct) => ( + +
+
+ {suggestedProduct.name} +
+
+

{suggestedProduct.name}

+

{suggestedProduct.description}

+
+
+
+ completeOnboarding(urls.onboarding(suggestedProduct.type))} + > + Get started + +
+
+ ))} +
+
+ ) +} diff --git a/frontend/src/scenes/onboarding/OnboardingProductIntro.tsx b/frontend/src/scenes/onboarding/OnboardingProductIntro.tsx index 144f8d2d82128..4cdb535e242f7 100644 --- a/frontend/src/scenes/onboarding/OnboardingProductIntro.tsx +++ b/frontend/src/scenes/onboarding/OnboardingProductIntro.tsx @@ -9,7 +9,13 @@ import { ProductPricingModal } from 'scenes/billing/ProductPricingModal' import { IconArrowLeft, IconCheckCircleOutline, IconOpenInNew } from 'lib/lemon-ui/icons' import { urls } from 'scenes/urls' -export const OnboardingProductIntro = ({ product }: { product: BillingProductV2Type }): JSX.Element => { +export const OnboardingProductIntro = ({ + product, + onStart, +}: { + product: BillingProductV2Type + onStart?: () => void +}): JSX.Element => { const { currentAndUpgradePlans, isPricingModalOpen } = useValues(billingProductLogic({ product })) const { toggleIsPricingModalOpen } = useActions(billingProductLogic({ product })) const { setCurrentOnboardingStepNumber } = useActions(onboardingLogic) @@ -52,7 +58,10 @@ export const OnboardingProductIntro = ({ product }: { product: BillingProductV2T
setCurrentOnboardingStepNumber(currentOnboardingStepNumber + 1)} + onClick={() => { + onStart && onStart() + setCurrentOnboardingStepNumber(currentOnboardingStepNumber + 1) + }} > Get started diff --git a/frontend/src/scenes/onboarding/OnboardingStep.tsx b/frontend/src/scenes/onboarding/OnboardingStep.tsx index 12d523eb78330..b32c9fdc13a3d 100644 --- a/frontend/src/scenes/onboarding/OnboardingStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingStep.tsx @@ -9,16 +9,19 @@ export const OnboardingStep = ({ subtitle, children, showSkip = false, + onSkip, continueOverride, }: { title: string subtitle?: string children: React.ReactNode showSkip?: boolean + onSkip?: () => void continueOverride?: JSX.Element }): JSX.Element => { const { currentOnboardingStepNumber, totalOnboardingSteps } = useValues(onboardingLogic) const { setCurrentOnboardingStepNumber, completeOnboarding } = useActions(onboardingLogic) + const isLastStep = currentOnboardingStepNumber == totalOnboardingSteps return ( - currentOnboardingStepNumber == totalOnboardingSteps + onClick={() => { + onSkip && onSkip() + isLastStep ? completeOnboarding() : setCurrentOnboardingStepNumber(currentOnboardingStepNumber + 1) - } + }} status="muted" > - Skip for now + Skip {isLastStep ? 'and finish' : 'for now'} )} {continueOverride ? ( diff --git a/frontend/src/scenes/onboarding/OnboardingVerificationStep.tsx b/frontend/src/scenes/onboarding/OnboardingVerificationStep.tsx new file mode 100644 index 0000000000000..7b55f2f139bd2 --- /dev/null +++ b/frontend/src/scenes/onboarding/OnboardingVerificationStep.tsx @@ -0,0 +1,51 @@ +import { Spinner } from '@posthog/lemon-ui' +import { OnboardingStep } from './OnboardingStep' +import { useActions, useValues } from 'kea' +import { teamLogic } from 'scenes/teamLogic' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { useInterval } from 'lib/hooks/useInterval' +import { BlushingHog } from 'lib/components/hedgehogs' +import { capitalizeFirstLetter } from 'lib/utils' + +export const OnboardingVerificationStep = ({ + listeningForName, + teamPropertyToVerify, +}: { + listeningForName: string + teamPropertyToVerify: string +}): JSX.Element => { + const { loadCurrentTeam } = useActions(teamLogic) + const { currentTeam } = useValues(teamLogic) + const { reportIngestionContinueWithoutVerifying } = useActions(eventUsageLogic) + + useInterval(() => { + if (!currentTeam?.[teamPropertyToVerify]) { + loadCurrentTeam() + } + }, 2000) + + return !currentTeam?.[teamPropertyToVerify] ? ( + { + reportIngestionContinueWithoutVerifying() + }} + continueOverride={<>} + > +
+ +
+
+ ) : ( + +
+ +
+
+ ) +} diff --git a/frontend/src/scenes/onboarding/onboardingLogic.tsx b/frontend/src/scenes/onboarding/onboardingLogic.tsx index c896cff3f396e..3f38d75747981 100644 --- a/frontend/src/scenes/onboarding/onboardingLogic.tsx +++ b/frontend/src/scenes/onboarding/onboardingLogic.tsx @@ -4,6 +4,7 @@ import { urls } from 'scenes/urls' import type { onboardingLogicType } from './onboardingLogicType' import { billingLogic } from 'scenes/billing/billingLogic' +import { teamLogic } from 'scenes/teamLogic' export interface OnboardingLogicProps { productKey: ProductKey | null @@ -13,7 +14,8 @@ export enum OnboardingStepKey { PRODUCT_INTRO = 'product_intro', SDKS = 'sdks', BILLING = 'billing', - PAIRS_WITH = 'pairs_with', + OTHER_PRODUCTS = 'other_products', + VERIFY = 'verify', } export type OnboardingStepMap = Record @@ -22,7 +24,8 @@ const onboardingStepMap: OnboardingStepMap = { [OnboardingStepKey.PRODUCT_INTRO]: 'OnboardingProductIntro', [OnboardingStepKey.SDKS]: 'SDKs', [OnboardingStepKey.BILLING]: 'OnboardingBillingStep', - [OnboardingStepKey.PAIRS_WITH]: 'OnboardingPairsWithStep', + [OnboardingStepKey.OTHER_PRODUCTS]: 'OnboardingOtherProductsStep', + [OnboardingStepKey.VERIFY]: 'OnboardingVerificationStep', } export type AllOnboardingSteps = JSX.Element[] @@ -31,17 +34,17 @@ export const onboardingLogic = kea({ props: {} as OnboardingLogicProps, path: ['scenes', 'onboarding', 'onboardingLogic'], connect: { - values: [billingLogic, ['billing']], - actions: [billingLogic, ['loadBillingSuccess']], + values: [billingLogic, ['billing'], teamLogic, ['currentTeam']], + actions: [billingLogic, ['loadBillingSuccess'], teamLogic, ['updateCurrentTeam']], }, actions: { setProduct: (product: BillingProductV2Type | null) => ({ product }), setProductKey: (productKey: string | null) => ({ productKey }), setCurrentOnboardingStepNumber: (currentOnboardingStepNumber: number) => ({ currentOnboardingStepNumber }), - completeOnboarding: true, + completeOnboarding: (redirectUri?: string) => ({ redirectUri }), setAllOnboardingSteps: (allOnboardingSteps: AllOnboardingSteps) => ({ allOnboardingSteps }), setStepKey: (stepKey: string) => ({ stepKey }), - setSubscribedDuringOnboarding: (subscribedDuringOnboarding) => ({ subscribedDuringOnboarding }), + setSubscribedDuringOnboarding: (subscribedDuringOnboarding: boolean) => ({ subscribedDuringOnboarding }), }, reducers: () => ({ productKey: [ @@ -110,6 +113,17 @@ export const onboardingLogic = kea({ return !product?.subscribed || !hasAllAddons || subscribedDuringOnboarding }, ], + suggestedProducts: [ + (s) => [s.billing, s.product, s.currentTeam], + (billing, product, currentTeam) => + billing?.products?.filter( + (p) => + p.type !== product?.type && + !p.contact_support && + !p.inclusion_only && + !currentTeam?.has_completed_onboarding_for?.[p.type] + ) || [], + ], }, listeners: ({ actions, values }) => ({ loadBillingSuccess: () => { @@ -130,8 +144,17 @@ export const onboardingLogic = kea({ actions.setProduct(values.billing?.products.find((p) => p.type === values.productKey) || null) } }, - completeOnboarding: () => { - window.location.href = values.onCompleteOnbardingRedirectUrl + completeOnboarding: ({ redirectUri }) => { + if (values.productKey) { + // update the current team has_completed_onboarding_for field, only writing over the current product + actions.updateCurrentTeam({ + has_completed_onboarding_for: { + ...values.currentTeam?.has_completed_onboarding_for, + [values.productKey]: true, + }, + }) + } + window.location.href = redirectUri || values.onCompleteOnbardingRedirectUrl }, setAllOnboardingSteps: ({ allOnboardingSteps }) => { // once we have the onboarding steps we need to make sure the step key is valid, diff --git a/frontend/src/scenes/onboarding/sdks/SDKs.tsx b/frontend/src/scenes/onboarding/sdks/SDKs.tsx index 737dfceacac1b..9ac4884dd5d40 100644 --- a/frontend/src/scenes/onboarding/sdks/SDKs.tsx +++ b/frontend/src/scenes/onboarding/sdks/SDKs.tsx @@ -1,4 +1,4 @@ -import { LemonButton, LemonDivider, LemonSelect } from '@posthog/lemon-ui' +import { LemonButton, LemonCard, LemonDivider, LemonSelect } from '@posthog/lemon-ui' import { sdksLogic } from './sdksLogic' import { useActions, useValues } from 'kea' import { OnboardingStep } from '../OnboardingStep' @@ -7,6 +7,7 @@ import { onboardingLogic } from '../onboardingLogic' import { useEffect } from 'react' import React from 'react' import { SDKInstructionsMap } from '~/types' +import { InviteMembersButton } from '~/layout/navigation/TopBar/SitePopover' export function SDKs({ usersAction, @@ -32,7 +33,7 @@ export function SDKs({ >
-
+
{showSourceOptionsSelect && ( ))} + +

Need help with this step?

+

Invite a team member to help you get set up.

+ +
{selectedSDK && productKey && !!sdkInstructionMap[selectedSDK.key] && (
diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/FeatureFlagsSDKInstructions.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/FeatureFlagsSDKInstructions.tsx new file mode 100644 index 0000000000000..6374992792b3e --- /dev/null +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/FeatureFlagsSDKInstructions.tsx @@ -0,0 +1,8 @@ +import { SDKInstructionsMap, SDKKey } from '~/types' +import { JSWebInstructions, NextJSInstructions, ReactInstructions } from '.' + +export const FeatureFlagsSDKInstructions: SDKInstructionsMap = { + [SDKKey.JS_WEB]: JSWebInstructions, + [SDKKey.NEXT_JS]: NextJSInstructions, + [SDKKey.REACT]: ReactInstructions, +} diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/index.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/index.tsx new file mode 100644 index 0000000000000..27d9e5388d04d --- /dev/null +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/index.tsx @@ -0,0 +1,3 @@ +export * from './js-web' +export * from './next-js' +export * from './react' diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/js-web.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/js-web.tsx new file mode 100644 index 0000000000000..8ef2865c3b834 --- /dev/null +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/js-web.tsx @@ -0,0 +1,42 @@ +import { JSSnippet } from 'lib/components/JSSnippet' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { useValues } from 'kea' +import { teamLogic } from 'scenes/teamLogic' +import { JSInstallSnippet, SessionReplayFinalSteps } from '../shared-snippets' + +function JSSetupSnippet(): JSX.Element { + const { currentTeam } = useValues(teamLogic) + + return ( + + {[ + "import posthog from 'posthog-js'", + '', + `posthog.init('${currentTeam?.api_token}', { api_host: '${window.location.origin}' })`, + ].join('\n')} + + ) +} + +export function JSWebInstructions(): JSX.Element { + return ( + <> +

Option 1. Code snippet

+

+ Just add this snippet to your website within the <head> tag and we'll automatically + capture page views, sessions and all relevant interactions within your website. +

+ + +

Option 2. Javascript Library

+

Install the package

+ +

Initialize

+ + +

Final steps

+ + + ) +} diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/next-js.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/next-js.tsx new file mode 100644 index 0000000000000..cda978ee12166 --- /dev/null +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/next-js.tsx @@ -0,0 +1,98 @@ +import { Link } from 'lib/lemon-ui/Link' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { useValues } from 'kea' +import { teamLogic } from 'scenes/teamLogic' +import { JSInstallSnippet, SessionReplayFinalSteps } from '../shared-snippets' + +function NextEnvVarsSnippet(): JSX.Element { + const { currentTeam } = useValues(teamLogic) + + return ( + + {[ + `NEXT_PUBLIC_POSTHOG_KEY=${currentTeam?.api_token}`, + `NEXT_PUBLIC_POSTHOG_HOST=${window.location.origin}`, + ].join('\n')} + + ) +} + +function NextPagesRouterCodeSnippet(): JSX.Element { + return ( + + {`// pages/_app.js +... +import posthog from 'posthog-js' // Import PostHog + +if (typeof window !== 'undefined') { // checks that we are client-side + posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { + api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com', + loaded: (posthog) => { + if (process.env.NODE_ENV === 'development') posthog.debug() // debug mode in development + }, + }) +} + +export default function App({ Component, pageProps }) { + const router = useRouter() + ...`} + + ) +} + +function NextAppRouterCodeSnippet(): JSX.Element { + return ( + + {`// app/providers.js +'use client' +... +import posthog from 'posthog-js' + +if (typeof window !== 'undefined') { + posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { + api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST, + }) +} +...`} + + ) +} + +export function NextJSInstructions(): JSX.Element { + return ( + <> +

Install posthog-js using your package manager

+ +

Add environment variables

+

+ Add your environment variables to your .env.local file and to your hosting provider (e.g. Vercel, + Netlify, AWS). You can find your project API key in your project settings. +

+

+ These values need to start with NEXT_PUBLIC_ to be accessible on the + client-side. +

+ + +

Initialize

+

With App router

+

+ If your Next.js app to uses the app router, you can + integrate PostHog by creating a providers file in your app folder. This is because the posthog-js + library needs to be initialized on the client-side using the Next.js{' '} + + 'use client' directive + + . +

+ +

With Pages router

+

+ If your Next.js app uses the pages router, you can + integrate PostHog at the root of your app (pages/_app.js). +

+ + + + ) +} diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/react.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/react.tsx new file mode 100644 index 0000000000000..86fdfc0f527c7 --- /dev/null +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/react.tsx @@ -0,0 +1,64 @@ +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { useValues } from 'kea' +import { teamLogic } from 'scenes/teamLogic' +import { JSInstallSnippet, SessionReplayFinalSteps } from '../shared-snippets' + +function ReactEnvVarsSnippet(): JSX.Element { + const { currentTeam } = useValues(teamLogic) + + return ( + + {[ + `REACT_APP_POSTHOG_PUBLIC_KEY=${currentTeam?.api_token}`, + `REACT_APP_PUBLIC_POSTHOG_HOST=${window.location.origin}`, + ].join('\n')} + + ) +} + +function ReactSetupSnippet(): JSX.Element { + return ( + + {`// src/index.js +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +import { PostHogProvider} from 'posthog-js/react' + +const options = { + api_host: process.env.REACT_APP_PUBLIC_POSTHOG_HOST, +} + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + + + +);`} + + ) +} + +export function ReactInstructions(): JSX.Element { + return ( + <> +

Install the package

+ +

Add environment variables

+ +

Initialize

+

+ Integrate PostHog at the root of your app (src/index.js for the default{' '} + create-react-app). +

+ + + + ) +} diff --git a/frontend/src/scenes/products/Products.tsx b/frontend/src/scenes/products/Products.tsx index 1b155f0974394..703e904d00a88 100644 --- a/frontend/src/scenes/products/Products.tsx +++ b/frontend/src/scenes/products/Products.tsx @@ -76,6 +76,8 @@ function ProductCard({ product }: { product: BillingProductV2Type }): JSX.Elemen export function Products(): JSX.Element { const { featureFlags } = useValues(featureFlagLogic) const { billing } = useValues(billingLogic) + const { currentTeam } = useValues(teamLogic) + const isFirstProduct = Object.keys(currentTeam?.has_completed_onboarding_for || {}).length === 0 const products = billing?.products || [] useEffect(() => { @@ -87,19 +89,27 @@ export function Products(): JSX.Element { return (
-

Pick your first product.

+

Pick your {isFirstProduct ? 'first' : 'next'} product.

- Pick your first product to get started with. You can set up any others you'd like later. + Pick your {isFirstProduct ? 'first' : 'next'} product to get started with. You can set up any others + you'd like later.

{products.length > 0 ? ( -
- {products - .filter((product) => !product.contact_support && !product.inclusion_only) - .map((product) => ( - - ))} -
+ <> +
+ {products + .filter((product) => !product.contact_support && !product.inclusion_only) + .map((product) => ( + + ))} +
+
+ + None of these + +
+ ) : ( )} From 16a0cf78203adc5d3fa295ffd2aa6924531962f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Oberm=C3=BCller?= Date: Tue, 26 Sep 2023 08:24:21 +0200 Subject: [PATCH 15/22] feat(hogql): add backend side filter to query conversion method (#17542) --- frontend/src/queries/schema.json | 34 +- frontend/src/queries/schema.ts | 1 + .../src/scenes/funnels/funnelDataLogic.ts | 4 +- .../src/scenes/funnels/funnelUtils.test.ts | 8 +- frontend/src/scenes/funnels/funnelUtils.ts | 14 +- .../filters/ActionFilter/ActionFilter.tsx | 14 +- .../ActionFilterRow/ActionFilterRow.tsx | 8 +- .../ExclusionRowSuffix.tsx | 6 +- .../FunnelExclusionsFilter.tsx | 4 +- frontend/src/types.ts | 15 +- playwright/e2e-vrt/layout/Navigation.spec.ts | 2 + ...ide-Bar-Hidden-Mobile-1-chromium-linux.png | Bin 33091 -> 33773 bytes ...Side-Bar-Shown-Mobile-1-chromium-linux.png | Bin 36042 -> 36759 bytes .../legacy_compatibility/filter_to_query.py | 269 +++++ .../test/test_filter_to_query.py | 1008 +++++++++++++++++ posthog/models/filters/__init__.py | 2 + posthog/models/filters/filter.py | 2 + posthog/models/filters/mixins/common.py | 18 + posthog/models/filters/retention_filter.py | 5 +- posthog/models/filters/stickiness_filter.py | 6 + .../filters/test/test_stickiness_filter.py | 2 +- posthog/schema.py | 31 +- 22 files changed, 1366 insertions(+), 87 deletions(-) create mode 100644 posthog/hogql_queries/legacy_compatibility/filter_to_query.py create mode 100644 posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index f15e50479ccdf..80eefe5462b0a 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -528,20 +528,6 @@ }, "EmptyPropertyFilter": { "additionalProperties": false, - "properties": { - "key": { - "not": {} - }, - "operator": { - "not": {} - }, - "type": { - "not": {} - }, - "value": { - "not": {} - } - }, "type": "object" }, "EntityType": { @@ -860,15 +846,7 @@ "enum": ["second", "minute", "hour", "day", "week", "month"], "type": "string" }, - "FunnelLayout": { - "enum": ["horizontal", "vertical"], - "type": "string" - }, - "FunnelPathType": { - "enum": ["funnel_path_before_step", "funnel_path_between_steps", "funnel_path_after_step"], - "type": "string" - }, - "FunnelStepRangeEntityFilter": { + "FunnelExclusion": { "additionalProperties": false, "properties": { "custom_name": { @@ -898,6 +876,14 @@ }, "type": "object" }, + "FunnelLayout": { + "enum": ["horizontal", "vertical"], + "type": "string" + }, + "FunnelPathType": { + "enum": ["funnel_path_before_step", "funnel_path_between_steps", "funnel_path_after_step"], + "type": "string" + }, "FunnelStepReference": { "enum": ["total", "previous"], "type": "string" @@ -927,7 +913,7 @@ }, "exclusions": { "items": { - "$ref": "#/definitions/FunnelStepRangeEntityFilter" + "$ref": "#/definitions/FunnelExclusion" }, "type": "array" }, diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index aa6e30283a2f5..7461e85ec75fa 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -195,6 +195,7 @@ export interface ActionsNode extends EntityNode { kind: NodeKind.ActionsNode id: number } + export interface QueryTiming { /** Key. Shortened to 'k' to save on data. */ k: string diff --git a/frontend/src/scenes/funnels/funnelDataLogic.ts b/frontend/src/scenes/funnels/funnelDataLogic.ts index bedb0d0172e58..078fba4da275a 100644 --- a/frontend/src/scenes/funnels/funnelDataLogic.ts +++ b/frontend/src/scenes/funnels/funnelDataLogic.ts @@ -4,7 +4,7 @@ import { FunnelResultType, FunnelVizType, FunnelStep, - FunnelStepRangeEntityFilter, + FunnelExclusion, FunnelStepReference, FunnelStepWithNestedBreakdown, InsightLogicProps, @@ -381,7 +381,7 @@ export const funnelDataLogic = kea([ // Exclusion filters exclusionDefaultStepRange: [ (s) => [s.querySource], - (querySource: FunnelsQuery): Omit => ({ + (querySource: FunnelsQuery): Omit => ({ funnel_from_step: 0, funnel_to_step: (querySource.series || []).length > 1 ? querySource.series.length - 1 : 1, }), diff --git a/frontend/src/scenes/funnels/funnelUtils.test.ts b/frontend/src/scenes/funnels/funnelUtils.test.ts index 16b92f99941aa..fac6a2b82f0cd 100644 --- a/frontend/src/scenes/funnels/funnelUtils.test.ts +++ b/frontend/src/scenes/funnels/funnelUtils.test.ts @@ -13,7 +13,7 @@ import { FunnelCorrelation, FunnelCorrelationResultsType, FunnelCorrelationType, - FunnelStepRangeEntityFilter, + FunnelExclusion, } from '~/types' import { dayjs } from 'lib/dayjs' @@ -175,7 +175,7 @@ describe('getClampedStepRangeFilter', () => { const stepRange = { funnel_from_step: 0, funnel_to_step: 1, - } as FunnelStepRangeEntityFilter + } as FunnelExclusion const filters = { funnel_from_step: 1, funnel_to_step: 2, @@ -193,7 +193,7 @@ describe('getClampedStepRangeFilter', () => { }) it('ensures step range is clamped to step range', () => { - const stepRange = {} as FunnelStepRangeEntityFilter + const stepRange = {} as FunnelExclusion const filters = { funnel_from_step: -1, funnel_to_step: 12, @@ -211,7 +211,7 @@ describe('getClampedStepRangeFilter', () => { }) it('returns undefined if the incoming filters are undefined', () => { - const stepRange = {} as FunnelStepRangeEntityFilter + const stepRange = {} as FunnelExclusion const filters = { funnel_from_step: undefined, funnel_to_step: undefined, diff --git a/frontend/src/scenes/funnels/funnelUtils.ts b/frontend/src/scenes/funnels/funnelUtils.ts index 46f50052b226d..8dfc6a0539e73 100644 --- a/frontend/src/scenes/funnels/funnelUtils.ts +++ b/frontend/src/scenes/funnels/funnelUtils.ts @@ -1,6 +1,6 @@ import { autoCaptureEventToDescription, clamp } from 'lib/utils' import { - FunnelStepRangeEntityFilter, + FunnelExclusion, FunnelStep, FunnelStepWithNestedBreakdown, BreakdownKeyType, @@ -225,9 +225,7 @@ export const isStepsEmpty = (filters: FunnelsFilterType): boolean => export const isStepsUndefined = (filters: FunnelsFilterType): boolean => typeof filters.events === 'undefined' && (typeof filters.actions === 'undefined' || filters.actions.length === 0) -export const deepCleanFunnelExclusionEvents = ( - filters: FunnelsFilterType -): FunnelStepRangeEntityFilter[] | undefined => { +export const deepCleanFunnelExclusionEvents = (filters: FunnelsFilterType): FunnelExclusion[] | undefined => { if (!filters.exclusions) { return undefined } @@ -255,9 +253,9 @@ export const getClampedStepRangeFilter = ({ stepRange, filters, }: { - stepRange?: FunnelStepRangeEntityFilter + stepRange?: FunnelExclusion filters: FunnelsFilterType -}): FunnelStepRangeEntityFilter => { +}): FunnelExclusion => { const maxStepIndex = Math.max((filters.events?.length || 0) + (filters.actions?.length || 0) - 1, 1) let funnel_from_step = findFirstNumber([stepRange?.funnel_from_step, filters.funnel_from_step]) @@ -282,9 +280,9 @@ export const getClampedStepRangeFilterDataExploration = ({ stepRange, query, }: { - stepRange?: FunnelStepRangeEntityFilter + stepRange?: FunnelExclusion query: FunnelsQuery -}): FunnelStepRangeEntityFilter => { +}): FunnelExclusion => { const maxStepIndex = Math.max(query.series.length || 0 - 1, 1) let funnel_from_step = findFirstNumber([stepRange?.funnel_from_step, query.funnelsFilter?.funnel_from_step]) diff --git a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.tsx b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.tsx index ea099b6ec9764..adcbb55787bb9 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.tsx +++ b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.tsx @@ -3,13 +3,7 @@ import React, { useEffect } from 'react' import { BindLogic, useActions, useValues } from 'kea' import { entityFilterLogic, toFilters, LocalFilter } from './entityFilterLogic' import { ActionFilterRow, MathAvailability } from './ActionFilterRow/ActionFilterRow' -import { - ActionFilter as ActionFilterType, - FilterType, - FunnelStepRangeEntityFilter, - InsightType, - Optional, -} from '~/types' +import { ActionFilter as ActionFilterType, FilterType, FunnelExclusion, InsightType, Optional } from '~/types' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { RenameModal } from 'scenes/insights/filters/ActionFilter/RenameModal' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' @@ -55,11 +49,7 @@ export interface ActionFilterProps { customRowSuffix?: | string | JSX.Element - | ((props: { - filter: ActionFilterType | FunnelStepRangeEntityFilter - index: number - onClose: () => void - }) => JSX.Element) + | ((props: { filter: ActionFilterType | FunnelExclusion; index: number; onClose: () => void }) => JSX.Element) /** Show nested arrows to the left of property filter buttons */ showNestedArrow?: boolean /** Which tabs to show for actions selector */ diff --git a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx index 90e01a83d6df5..921f18b586f22 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx +++ b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx @@ -5,7 +5,7 @@ import { ActionFilter, EntityType, EntityTypes, - FunnelStepRangeEntityFilter, + FunnelExclusion, PropertyFilterValue, BaseMathType, PropertyMathType, @@ -89,11 +89,7 @@ export interface ActionFilterRowProps { customRowSuffix?: | string | JSX.Element - | ((props: { - filter: ActionFilterType | FunnelStepRangeEntityFilter - index: number - onClose: () => void - }) => JSX.Element) // Custom suffix element to show in each row + | ((props: { filter: ActionFilterType | FunnelExclusion; index: number; onClose: () => void }) => JSX.Element) // Custom suffix element to show in each row hasBreakdown: boolean // Whether the current graph has a breakdown filter applied showNestedArrow?: boolean // Show nested arrows to the left of property filter buttons actionsTaxonomicGroupTypes?: TaxonomicFilterGroupType[] // Which tabs to show for actions selector diff --git a/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/ExclusionRowSuffix.tsx b/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/ExclusionRowSuffix.tsx index 3c657491b1134..fbb1f61619023 100644 --- a/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/ExclusionRowSuffix.tsx +++ b/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/ExclusionRowSuffix.tsx @@ -1,7 +1,7 @@ import { Row, Select } from 'antd' import { useActions, useValues } from 'kea' import { ANTD_TOOLTIP_PLACEMENTS } from 'lib/utils' -import { FunnelStepRangeEntityFilter, ActionFilter as ActionFilterType, FunnelsFilterType } from '~/types' +import { FunnelExclusion, ActionFilter as ActionFilterType, FunnelsFilterType } from '~/types' import { insightLogic } from 'scenes/insights/insightLogic' import { LemonButton } from '@posthog/lemon-ui' import { IconDelete } from 'lib/lemon-ui/icons' @@ -10,7 +10,7 @@ import { FunnelsQuery } from '~/queries/schema' import { getClampedStepRangeFilterDataExploration } from 'scenes/funnels/funnelUtils' type ExclusionRowSuffixComponentBaseProps = { - filter: ActionFilterType | FunnelStepRangeEntityFilter + filter: ActionFilterType | FunnelExclusion index: number onClose?: () => void isVertical: boolean @@ -28,7 +28,7 @@ export function ExclusionRowSuffix({ ) const { updateInsightFilter } = useActions(funnelDataLogic(insightProps)) - const setOneEventExclusionFilter = (eventFilter: FunnelStepRangeEntityFilter, index: number): void => { + const setOneEventExclusionFilter = (eventFilter: FunnelExclusion, index: number): void => { const exclusions = ((insightFilter as FunnelsFilterType)?.exclusions || []).map((e, e_i) => e_i === index ? getClampedStepRangeFilterDataExploration({ diff --git a/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/FunnelExclusionsFilter.tsx b/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/FunnelExclusionsFilter.tsx index 9bb147c049967..97e93ecf02702 100644 --- a/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/FunnelExclusionsFilter.tsx +++ b/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/FunnelExclusionsFilter.tsx @@ -3,7 +3,7 @@ import { useActions, useValues } from 'kea' import useSize from '@react-hook/size' import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { FunnelStepRangeEntityFilter, EntityTypes, FilterType } from '~/types' +import { FunnelExclusion, EntityTypes, FilterType } from '~/types' import { insightLogic } from 'scenes/insights/insightLogic' import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' @@ -22,7 +22,7 @@ export function FunnelExclusionsFilter(): JSX.Element { const isVerticalLayout = !!width && width < 450 // If filter container shrinks below 500px, initiate verticality const setFilters = (filters: Partial): void => { - const exclusions = (filters.events as FunnelStepRangeEntityFilter[]).map((e) => ({ + const exclusions = (filters.events as FunnelExclusion[]).map((e) => ({ ...e, funnel_from_step: e.funnel_from_step || exclusionDefaultStepRange.funnel_from_step, funnel_to_step: e.funnel_to_step || exclusionDefaultStepRange.funnel_to_step, diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 0d529adc829cd..275561203e954 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -586,10 +586,10 @@ export interface HogQLPropertyFilter extends BasePropertyFilter { } export interface EmptyPropertyFilter { - type?: undefined - value?: undefined - operator?: undefined - key?: undefined + type?: never + value?: never + operator?: never + key?: never } export type AnyPropertyFilter = @@ -786,8 +786,7 @@ export type EntityFilter = { order?: number } -// TODO: Separate FunnelStepRange and FunnelStepRangeEntity filter types -export interface FunnelStepRangeEntityFilter extends Partial { +export interface FunnelExclusion extends Partial { funnel_from_step?: number funnel_to_step?: number } @@ -1682,6 +1681,7 @@ export interface TrendsFilterType extends FilterType { show_percent_stack_view?: boolean breakdown_histogram_bin_count?: number // trends breakdown histogram bin count } + export interface StickinessFilterType extends FilterType { compare?: boolean show_legend?: boolean // used to show/hide legend next to insights graph @@ -1691,6 +1691,7 @@ export interface StickinessFilterType extends FilterType { display?: ChartDisplayType show_values_on_series?: boolean } + export interface FunnelsFilterType extends FilterType { funnel_viz_type?: FunnelVizType // parameter sent to funnels API for time conversion code path funnel_from_step?: number // used in time to convert: initial step index to compute time to convert @@ -1703,7 +1704,7 @@ export interface FunnelsFilterType extends FilterType { funnel_window_interval_unit?: FunnelConversionWindowTimeUnit // minutes, days, weeks, etc. for conversion window funnel_window_interval?: number | undefined // length of conversion window funnel_order_type?: StepOrderValue - exclusions?: FunnelStepRangeEntityFilter[] // used in funnel exclusion filters + exclusions?: FunnelExclusion[] // used in funnel exclusion filters funnel_correlation_person_entity?: Record // Funnel Correlation Persons Filter funnel_correlation_person_converted?: 'true' | 'false' // Funnel Correlation Persons Converted - success or failure counts funnel_custom_steps?: number[] // used to provide custom steps for which to get people in a funnel - primarily for correlation use diff --git a/playwright/e2e-vrt/layout/Navigation.spec.ts b/playwright/e2e-vrt/layout/Navigation.spec.ts index 5c0258c33e694..2af80117af5ad 100644 --- a/playwright/e2e-vrt/layout/Navigation.spec.ts +++ b/playwright/e2e-vrt/layout/Navigation.spec.ts @@ -6,12 +6,14 @@ test.describe('Navigation', () => { test('App Page With Side Bar Hidden (Mobile)', async ({ storyPage }) => { await storyPage.resizeToMobile() await storyPage.goto(toId('Layout/Navigation', 'App Page With Side Bar Hidden')) + await storyPage.mainAppContent.waitFor() await storyPage.expectFullPageScreenshot() }) test('App Page With Side Bar Shown (Mobile)', async ({ storyPage }) => { await storyPage.resizeToMobile() await storyPage.goto(toId('Layout/Navigation', 'App Page With Side Bar Shown')) + await storyPage.mainAppContent.waitFor() await storyPage.expectFullPageScreenshot() }) }) diff --git a/playwright/e2e-vrt/layout/Navigation.spec.ts-snapshots/Navigation-App-Page-With-Side-Bar-Hidden-Mobile-1-chromium-linux.png b/playwright/e2e-vrt/layout/Navigation.spec.ts-snapshots/Navigation-App-Page-With-Side-Bar-Hidden-Mobile-1-chromium-linux.png index 0e709cd227bebd3f14dc6c5fada45208a84c96c2..1be473f7dba29a8fd2afda439ce03c9ecb9e0993 100644 GIT binary patch literal 33773 zcmb@uby$^ayDvIuL{cRLDM19JTR{*cr5mM7q`O-I5kX421OW-@knT>UJETN91SHQr z=C{^5Yn^MIz4yAVJ^z?F z;R)&fw79sqdq*iKEd%k3;^o&6O$DC`!?pr)%=$mcCW%c<-fC4+%ookhUghBA3-b4; z4Gj%NL5LZ0hP=lh3<)5_4#}$u$gFeY36}YGEl`Y}fuXshgS^6G^!2-UGU}uA7 zcXxNUGd_y`#D~A+{P?-Jg3|qoaPj8j%!VAe+rH#{nA=VX++A13{-*HPgXovMoSdPd z@^#6eym``e>0s9#2bvq#9^{&cYcp1DRDJ!*g77OSuqKYn^m58N4+@fm`$7xvT*_SA zbKgy~EIjg&3-e!K+WDR(kAmx#wKgsh8(U#$^AZIi{p?vwe-i)R$(}L&y?ZE#J9qAU zZ}9dM5EN`}Zbm^UDMh?R_3?>`N%%;K;Yt~ooE(aYN3NDc?kNM$u)eIgrSx3l9{JE4>iAQ6CHkC7e@cX$oE#0~4YlqPJiIW2mlo+f#b{5G zvW>LjW9ogPV|rURE5@oWC&Z%fYA`>2N~aaCjf(L3yFwZRAH~7JAtWac!K2_qM2wu^ z$(nc2*1UNw_(Qg%Bb$R)xv1LI#o({4if?TNAO5*EmK^WNg{68eK7!`%UDR-D5e?7U zB!1hw@B>2WtkIirR!YBBe20u8sbxaCUSH5iG8Ecd+nki7JjzBpJ?TEyLU`~GIB9t(bgNidX=eRz|O|@TAJmx77Hq3q=3%7+Ie}^ zGxcT2gJZe9HJQ;yR2M4Frw2jpc3Yp4tcQkdKJeSlebipt*m&=chV`?_AC2&RJ2oz^ zxPk({Ldv7|RVGHpCYbKUg#`p1oiWb#D1x__u%5QwJugI>UffVwI3y62uQ-1wLzzhO z@9JH8f$R0jYW(volNuD}woV6^!6hY`Iw6V1LPA2jzkbjq3)rK6QAla|EET@6xJVnP zSX1DnFV26>(eJg?U7T%k-*~e8?LR6kw?0WXz13o2J80Ol3fbFgWAWlkF5;SR!5bso zFgU9%7pX0;{mddLD43a@UE15--CbN(7TtS)wz9loFVh0s(a)7XB+|E$to^x>MJ(-U zGYlyvX3qxm>z`uSfr;VUl$(*)Ww|-WwoBBigdfDcowg^jv&pCE=XRy0H<~S4ocOk( zt@VQOYCrm-WjF@KEK$Jm5`SC7%Rhj0-R&I|~E^(YvEzV5| z#%CmGT$HVOSG9~wC%J9`m(zsWjTjc%_E#r;z7)7rA_N#^gWcAe8|H)|zww@y6xf;) zSE$funJiv@Rul4$_0PCVD<>cJy)ujPk?Skmw+y3>N*}rM5{+S$3(#G1ec0T)*m;^_ z--Lu$Z+@E+D6e^PU$XS_e1piOZlF23%0$4`91RN#t2#SDvq&%9eyL;AVW}f*@Av3~ zYHt}Vo@>r-1irz3Sd4KOM@FxDKQhTyzggsR4 zPx-xU;c}mMm$Cj{Hmh$?!ThQtsu24hjEIjY@A2jwQ0Dy75C_ZDC{Jur&-b{G?XQ%X z?-%4q2YZGO+3hHk-x1__J&)*QW=?UDu_a-?d*hn2&ew}1-#3pP?ALOMGIn~XYjeng z>z-eqV{?BYQdqOUf1Suj2=jJP847~mrn9+PmZIEtUXQIxNYyPeM!!k=rBU1wM6ZW`GWR)204CXHJA(^JZWcqZ#3h8v=3>n#qN5TgFtZmEC0YxtPm zD=W0O43*N)y2Vs}Ym2wHEzN!AL41FYuzmIE+*pU@Yn2|Q>eVs+4_D$V)JKl}9%WtXv$UnGU2nOLPA1f z=i-8iD7eHKBAINW;u*j~dE?QwVm{hJRq$eh&OnLajxfoaEm|UM5h^qe+-!v$AK!zP zJ5P3>(Q6vVF{|1gACZ3w<64~g%l%?9oSB!mNjAv{VQ}7+`bud&M0f3b8wtL=)sJjb z@69<+jAN=S^)m)?s_-vPBPRCt%GEZhdA$SKWNAKKniKN-N2+wU-s2irY1wybF8BQv z)cW=p8__hI#fpkq-I}Y)jd-Hxg90U=kdUxxePe(Fx22_pkc1@LsSyPQ1zC@zvCKk3 z8;;`fHYt3QTgFE8O-+`*%@VmCi4C=llvY2^4n6j-PvJ$y{Nrc*{LrwU!Ki9!(6_4Z@+Jv&FaBwavxVl@|1)+6*+ip(sf@O!Do zE%UHCn55(;wtP|V2Z!4~5~CYyxHve}Y7{ahHKh=Q@5Cocj7ftCuD3nW_hmWvA{rXf z%d4~T$R_BBlJK`O!1DWPB%??T|5Uz{pd1(&*gWT| z-f^=1GU(#d(Gm|(FTqr5cM7QAS^%kz-rt!;Iw`4IWJ?SYuGZ|hqFE{A#BU46g7 zG$Q=o01S?W`^EWrmUzN!qC?!DNiqrvaoPy4==*riqRtOJ{c{i=r|qTA3Bmr;q)KZ-LJoAr zG~qy{oVkkiAxZm&@0qPff!!i-X(5(M#v}InyPpM zuduMNd@s}$M85K#lzkQ5MK_sff9}oIz~o?9G+RB%!54xcSgxB}Ungm6s$?8eZAk%h1_dXesZ z?TfbD<^%C43ZsV)ALgij@sNu*u`mj_>rc2|i!+e4rG}nVp}W)7T@Gk#t!ZOG$kkSI*kY*UlJ~c7@;Gj#}5#B{i*qZ$&C4 zA%2(EL00JN(@I_agiG%lgUh~VQ%3K0FhbsI5jJYWc$OtOfle?rv9Cp!sbyFBKK88o^ql)((9R2NQ$97;WvTatpt+ za22BC&5GX(si(31`YE{@9^SVjcfpm{=#P z>+Ka)P$1zdxpiwfcaxaADgXMad>ny6%$nJr6&= zeR@;dY5P0bF?E*F@!SIdML76j);|s#eYU%EpI6U=pTPunhRp~5GO|$pwhcVgh z+v)O2W5pE}q6@)_yIcItggAlZcO zm1_twXj_B*1k)^Sy3$6OAYGg80)*H2Qk|ZINde>=F07Rw8{+OzOJZyV46e zxC>@%Jg4rGyf@7GWE%9vC^aF9O;S(1Eul=S{1vU$*8+R`bH=H1>(4F*zA;%<=3N8X z4rf)G80LPp$229+EWWZPO)kNW&v-`U@Kov7vsEjT^N8+AtF6mV#`df6Z!xNvuGw~> z1@wIO+zivYu!#M;@FrGEm^mbx0dLnFO#@B0I@(zl%}eya)Zwuyy*y7yZOhgyMbKo~ z>Z@v zqRnEl-dpMjmnhPET;kAv426-(4O7VLc=krcw?>Mh6WLD+tdz-ArKq^Oc{SPC>;2z% zZ*o36NEX;-Rk(9PQo%y3nCx=Wh@!M@W8JGBlqcZ3^eE5MsKRnuq0uI%L9X|8Su|@b z9nO5v(aT}S_ZF=lHXK@$~3#FnbBSMHxh3Vg^|_zvJQN zZheh$P;Ag#$3=bRp2#%BO*|f15i$|% z>$@dnm1$ni>r40u20T3FbIdu7NOQ4bV zj4t5)y=LXd{`^gx^JPn8mHPt{`6NCJrN*Vz7P@cD$X2NBp0XZgDzElD zR?XT-ArfN;$U05J#NPuo4Jn>7N_>;+gtJCt+n6MZkIUIPwxtx<*wog)S2LZSj0^ucwxCQOg?{4R*gLvlissc#UN>@ac6_h1%X|L1>CPO-Ozjmy^@~FD zTWb8Td>4yZnJ5i4V+*%~{H5vD+*C&=CT`s=DQ;Pf>vpsJdbsDQJB9tgS}!5$qA-hB z9VXh26v1KJL0Ax*UwY_Z8BXhCgTyIi4JUZ2oo5EYa;{RQye{Sj)IZlW3M{r%Rtv%+ zef8Zj#ELAoRfZ5nIhcc(ZoC$|MmM)-5A2Qaf2u8e;NVboU^&z1N?rDHKy*W6E8^ov z9ojXuNE6H5--l_Vymw3X^_V0jdo0Hir(CKkhn%0*tBaJ#Bt4^0`iyVxS5%m-u-hJS zC7Eb5P3eDan4jj{4fhK}+U_}VOL(V2%OPHJlOCm606Kkz>o7yl(<;3`v5_%Q)8{WV zu+lBbGpYn|su;^krO}5~EtNRV?g+Zaf20*b>9AF#E823|72o;J!&~ccT~8ttMep?L zw>Tq9P)Q_{o6z4RImsYNgHZVj;V);Ay&Ay{pHChokfF^^tIsKUOYjaSzWR->FW_Pk zv{gGubK%TanH!JWSJ=S&x`QAmme=L^%YNMETL-rW)(1d_QRn@d&*ZwXdS)hGX`>sL zvOUfrd?RS*#-wx0*ab}?tNUpviU<{E#<8RAbxY@!O>QQp1j@e5XT#a@iwob?KJUkU zBuw+uD2SQI%E4z|x)X{UW{~mITyDyvj|y?_Ssy#pxrx#g&h5B3QL}Hcorg<(Oeg3t z3b*58o2uS^FYnHeajK=?6<^$9PeKmj>tp*j7q`bMr?CD+{X^gu`Otpr+Kuzyni(~- zd3h~0s`@3O`;8!vcgXi2j1R1YKb^~FHt@cQSiogGAyU4#EiBzO*!V2bdUcRY=XV`< z=IM=9=~cHl1q``n-A-jY%`|_1ls>tb@tlJbAt8B_1p|G=b=5y+Vd(Vyir**;3(A=! zEnMwL$S2f_sHDUju^s7EQ8%oU1asZnj5yIz8(LNq(5}dhhkaxWDtY*&Y`+-c{%V$5 zi_JUB5K-io=tJWUxWk?`f1AFa^VWonI(Hr#*(hh`g2Lf@C3^ItD(sGq9nbC=AB!$b z7ev?E0U?p`>tM2*JUQAFN6ziPUKlAH#U)@?-89Q|@>7u+4kG?7Cn!kkM|B)a%d1(U zG*rljw|h3od=>x2jGX52Fv2x8_Daz(UXvEn^Vi(cnsmQpbIPwQqjZFkB(CaB$7OYc zqP8s2orN*w<5VT_U_E@Jq5BNk420M`8;i;@=%^@iaw}^-xwv;D2?HfCb~iODlfL}U zv2Hf#{ASBkd^1I&-qcA)BX-%|t+H~|an4zn>1x)^(W|~4M+|SNE}gd6HpQsULAhFZcW7fL@e(iA9Zot zQd_A!^Qr-RM_V|~!jQ23;`v^{FLIDJ{FE5QZEV=~4-RMv)xry17#KQ=%gX~|Vqz4~ zkD>zZq6x`xwU_Z23(0ed8`2VtQ$6z-ak8Kl*R|wi=_5O|XA|4#kl^4PyTY<1ybx_sP z29?Ti|9e&P|5vX3zw!oG-fvVwvAnrS%cj+freS&-{>D66Rqp4{QLQQc*3%Fdvpn*R-@*_9Ki;Ta265ZY1ZL)2=&=_rpQQ!zWtG-W9Z!l482m+N= zVt6>gSfOsdLoJl}#rNzD3z;BN5!_^u`<^M_(cjsLRisyM6xZ6)@;&|OTV@ubA6WR8 z>bj^I(nI$BmE%>bb9w3y(9zNDhCs91JzQ7KnhqAfYc*Gw5dG5DHeC6$r1^_G^&Lxl zYHXkqf7D1cec2a}!p+UCFqJKz^l??hWs^*=&aGJ{>7gTooVC6d2Hf=#aj=8z{JCGB8p6xW64QG7*Y=5vS4}vTj0v**~XKQYDma^XC&(F-2wY8;_ zy(IzneR>rYm57)a3~g;~9_y)FK0ZFX2M5510$Use9zL{>9WT-+u(h>Sn5uSM<8a;9 zQJ9*ob(Nh}1nI8WuoY8WLW1_egX>U|TMyTVV{~sk> zEseS(oI3o0X5q(txzaTa2XFId+xnB7LXRI4-MH}@6u`TztaxK%V|K2to#p1k->dA6 z5l?hdSF^sw11%{!>Zd4rkt4Z{vE0@U%J3(aq;T(-!EhK3^I z;;>|8WqEAp?xd!sf^->BQQ@3Wc%An2Nxtf(&-q>_HXfdYk`BYZvmR99wi;ZV{@HYncqS*;7R#z__jlhklz(rbG zTkYZ9^S^6pZhJp5R{N9QW@Ow|NCl`u$HjF6ruF{j*4D~V!MbO?aVNgjRQa{Lk-^ca zU^mpd?)==It%bX99~8L?h5xnmUw=}PMr))&?B@$oq^J22=VwmjhGzG*pL z*dBFHR={6Z10GVN;KjYT!gWvY{biZ+{a$tl|N6{tw(~}-eF@UCvZ@K;A7`afpNO#b z3F_T``8BrhxqkGk3qwjYOkBU{=$mWzuV7CttgHlrE5X;_)`nj8vfn5U0~Mv=d|y^U zuo5082#+EN_7yD~8$K2RmB~0MDd`d{w~ot;Geaw@uUo?rH%UpcV1L4%{-j&$vT*SC z)sGB`-06Dv1Ig{VHy|1Y5m0~Nvz{tm`*Z%+F|0R-F&Op9ix-JHx8W{7j|qSN{P{

l7M6LKvJ?EHY$kC09rudA<*jEZ_)?|JO7uTy1*@-G69kJzdkr=_J$c1TQ2 z{J9h^BH0_u8kv|Ff`)}p!_QCZm$`1%`GNA}Xxeu4ky%DV%&>p{v7eWZdYpJgqQHS^ zpwK(>=GPcx{5E%Ly^gnaXKG!;qs>NglwVC0)7E-jp1W6XM~Yk=-mI&un^@7S_pomu zym8|O;qBY+;IS>wZY{?rCDGB-XZFn!3?@HB6HYtp*8gThDSWbk<}x`utGi{8-}6cI z&GHZc2?qJZHW^1}=k|C`F|=#DpuYNOM??e^7 zLVM(kdW&hjmBWT`chatD4CmxbDB_6xkY2L;c}7j)^E-h=H~!K14w zx1#?3{)ZbkZr*&DRx(zgy#!Su_}x2EIXT?RziZTiYf?SzY|r;!HI!WXl|_k!5hUMJ zBo2{9eOZJt(1Cc6jTRjwJ*HoUPez8={t&gIcQre>vwq@3U7t+fi2o7eh}PS0B@IQ{ z)$h3_0*#8pu=sbWZ_V<>#(}e+G;VUp>S}@jSrrAASN-i*YSa^DsCFCl&l{X5;`gpC z{P^R2>0{GW^xhxqT9kv35AE|K7j@m(5Ea7@;o-!b>i1e^2@udZ@+D zXP=AH{anxrx`-%3y)RigLF zqkQ3O3Z&RSi%sMpbKAw$DpuIrXI|`cmF;e3kxm`2ZCN-Tu)Cgp2HCJMWt14 z!Oxqw+r<{%>-#m1BZfs{^>>c+Q-1vr{kxTU*B0^kD!=5|A&TeigqF;u{yZiJUljTa z(bLio-<-_<|;;u*V?L{9MnK4SQbV%+uZXa;$e{4YVJpHya3*H{U$1$uihEzLa% z$BP;-Ltw`g_adisYoR7XXs4p0f>-S6(-PZx;X98#)E3#RW@dB(-o8Zwj+4c=)C)^X zpDVbck8OldFNN^?$Z2yQ_TG5!Kk>FHoOauCMCjkM066>@G`TTYr_zZoVLXRq< zMt*KgBXV!&*#?vEA`IVAn`b#?pdth+V$0Wdgqi=Ur!!FzC>=%?dx<*}%UhXWeLiDJdyET#Ub&H52G$W%-mpeywTM@b@)vGFKWnUk%SqY{dq5sdIT#KrTY`Ve)P_lev1a=P5S_4$X85S92M|=Rdf}&S)*U7>pBgoF@!{SR zzdqLJ87~>`5Gl=teO8&NQBS{wcbU0Q)jb7i)+>HakZTEQ4sn~U;C_-|`WB-hhn2{W zdMSMWT9L zBHx`nj*0fB)`*5tX^4Gj$r z61^n-qEo!VrT;(ipbSLEr+wZ|A% zQ&&FjD~HGYkBMWxm9G;$E7U(8KNH^1JF-7i8U16)E@Sq(HaC|EFkRr@QfHaZX)u?dFevXN_LrGa;InFkcuc>G% z3^{UIT~+5|GK^a9ouOr54d!;<`C|%K>+yikfUjYN+xmw=wg0K(i-} zRE_SJT)%ElclR}^aH{qa<4(KZBTSi@nKS^3oj1lLpkywrtzr54`o;`5DJ1e-E71NX zDJ_kiD&&q@qSbd)OU-8$-}5pg#K6Yp18|I`ws$uLoPHsJT|`=1H~`_FpTmV)z_+wt zkNR*Pn=hW&Fuv3oE;ZlaJ;~%79UZN#r47IM8*K_)bFQ*eS@4Gsk`4}BQPI&q z>f9Z@_S#94gSdL2q#ajIN6BV3TkFoo?PHK+h z@`+~(_2=-r;6(0QU#;eQPA+snd5g#P$?CR-h z>7*Y2YG`L~Z*S+|koVgtCo_yIuPht@v=dm3eP^s2e0j^Ot7BDzG`W-T1wazSf$K&^ zMJHYbS822)ou5B_=q`EDK` zCq+E-e)gD_I869WCc}?sNoNqDtDIf0&S3ax za}rHNL|`VGrp$20FB2%%8JzNo6HG3xoq)Ya9su&n8vQlTs<0k~liHVp(NLdkhU9A|ka zB}c1sJmz7w6Z>GgZcSmpB7mzK3}wnl%g897xjk7QPmN%1eu!01zsnOhxNyio}P}|%FzdupG-9oGhAk-SP(CopOeiPe!3=t`lmoJf;MKYgKi59Np%h%5o6%}==9WVg9H^Tzi z>t%1GdGv_fb$dn>M(X!y{#*Do6HD(~YM)@!-dIDp4_MYsPNt;b>n4=-_jqd>OI1}>A<@bQo)QL*0%Q-Q zY%-jU=ke!vAV99ygKMqB!>a2IAOwX0Y?*2Bra0c2f198G;5P__zaQo>+q%b?rI}8? zgj$Ux#&_xHkiFLH@Q0LEu>neLL;1$ney(R|`rYgVlP-4hl}uYY9!Hzk(v^a>zu7D{ zzrljiBmwMtRQTodaLuWT`SBzqXk%dLv%S#BL8#t$^Uv;XZ>es() zJCBuz-aD9{m{@2Dyq$rDt& zce(rKz?t5Dym9A;_B{1`LQ2Z8wl;C7T9`1&fzU`^bgDXUp)A1mX#?h%H|ETcdG}St zQW1#wJ|~Mu$*CFGavap)9A!r&SdFt0~Y;3@UiXAdm zNZ`VNxy8oEmjvtt%QQ41g7C;ueQNJn?a^TB<2U{&{oA5>ALfK#h8vQdoShki6!^^2 z4g%;!hYnqH$T>RNe2yI`=C#|51(|a(>|7_0+cRI}5>R2=e$}fd@960G_t&S9;Dz`0 z>>e0M|2wbgKa7_DuL<=(c_TBXa}y+SqzM2{n^B1tMJ;_m1bKT?69T3Pk}47fFDiKE z(mI+iU~hKy^@+nKM?oBp2#ZO}-w%6(+uBtRs^r;?cjB8@Swq9a%^bKE!)f>?cLV9!mZaMw0 zMgkAH0lOmUR)+NJ-25fplNdq+fgEZ(#IYkker)SLXvU&8Sni61+G#ZfXnt{Hqqm!s zkkIdW*-P`RI;7c`nGALd;jLQ%6#O<#P>!LXwE^}gf>&V2E6l;akR3QigAaKEK}A8( zW%G%Tk8s1qk#=922sPA;RuE`+K~zM-5wmVKJGnsxyo?cjSTI#^ldhw!VcsN=f znvGvp*1baA+Kz7RFA9|L2?>i|(tPqhMqd2uP8=K@46Lp3(B2%UkHBn!{n9lsu)17P zTG|4g8JX&b_FmP}3$`wySWhlwz>)8dw6z;?mX?)4@nnq?p`k%Q`)vas3ywu-r~lh+ zjMRI&E`!4a6v66+n%XU}Q5RQN8RF79mlmNAN!i`D@@)8BkUP?0=#wBnzMb zX0)2F!~?%crDhhi?eCy;!0(6uuygtQMKLV{S}r+hCWw@;W~w;BSOVIGkAVCaJpgY) zV&eAh2e)vLaopwQWu$Q@0KT4?!`WQDCmN#T1Lb=#)f_4TcCi^58$$%%4;Dj@wW>VUB-ZDvE@)?lGJ{T{h?^S**O=%i|~GAR!AZhNRu*P7v* znmBNgtquAG9G4K?t{Y{wbyl0=ja6CmV5%9oms?BC1gU={JkW3SiRZH>heHkJZbvmz ze3{H1ZK;R)>vTsZS1}DhR=-zTYlroutM(&-?L>toX|`foA(y_D70a~VA6P6pwJvzz z`$hG-9{y5BLUE`V2zp=il9G~V$Cl6riRdI>*KvNOhvEDQ<>h3%c89~D z34wS5g_RTBO3+tm1q9+%V_~u6=?bqeM5^a&q(ep&LC^mUX(}OIKz-i}=Y(Eu%3QeH zeO-15xZBBN2M}VAs&t`#V_}8k{k(Oxq*@R*`}+EdJ^ol^rJRE+#irkY>*(kR)PVpF z=(Iq`P*ziG>(*@a@diU?2@Gch0vxyK`--K!w;_&61ms{+Q)0fq5NGO^)94IyOSl&pDzXCL=5R6q+HZ0~0>8E-o&n z!cr>2kBDaqzJBF!{;ekDaTtu(b59pADx@8L&DQdN{rVNoRrtStjVdlB<+z`al++4b zk$_653GRmwAVyuC5H95-we>ydBAiBT*hkL03!(+7X>8xe9_AhBnVOlkc64CER9PjK zUsS;MUwKrh_8c=jM|Y!CXSVPZ*hs?P=yOqw zdxaLtKws0hu|(-ggN?Bl}yE<)?3=w`De|+VGTD=Phe-4e(CA7AMQMSeSUtvpUl97m6BhL_5@@SPLIP^ z;LO?$u|qt&rA6!nAY^ycJ*!i2lk9)CUDK&?lCkkwMviEe>kieKueyVyI5~l#Hbwy3Gk5{-{)KaG7ZS< zhV|sW+U)|6r-o0(R8Th>SuCRppZFCQa{$;kK0RCquQYkc`c}Or0vQ%S7Jh(!NH+I@TI2&TUL>@B zHC~t^xx%uuK%~A>@L{CB!?aotHV3k@pLxK-tUjW%gbo94)JqZ7%%AIhGz<{RG0no; zjl!Q{zO8~BWjF=~T^=o7!2T}q@>cv8kb}4nDg_Z4S@4g~Qcvk`75G&MF{u3KwRpyme zdW0|a=y?iHSMP_TQ3g2V1lgfn*f>b%pAGD)PlD#|^0tug#lbrnGJ_296|3nr`tS1U@oAQy<6VP4vzrTa0&o9 z6{Wtgr3$a$qY(USB{ae262VH*dY;b?}!~( z^%52q%(dr7TiPtzmAIhp*f~13hTi5D*g&D-;X$T)fGl-@B((dp{kltv)y=RGCMM+HtyamjC+k zd9Rf~gmLx?DghDXpmrM0@UPAf2p}mH4kZTUhJ0rsQ)J6;fW8w5sx}f=Z=S-s{Hpy8 zAIt(#5HFD|EsTP{R9+Bvbv(qcBtyyk;kH|V-9FIx%6RaHPTY)>++%~V-dRIZ+P-%q1qWDM4|H8Oe)jv8{S z0~mBWKjDP_h(MmV1^6CRTPtXm26KoL{s$>@nVC6`9k>Z8;AxNUjqHK%a#u*`4v0K~ z@7`VWoayV6>q~wZJ*>d)-F6$n170_*EM%?<==$mLPB@V6d~Y2wCrXTC@>QsG2znH| z{pD_CNec}NLy>;14{#6({&#l5fqFMVpa*n6wB5_eh|oUnyIIpr87z&PE_oj`6JZH~ zn6-pt2Ovx=kL^rCD!z_fTIfA?LHgh8@=iiyN$Y1kH_ic7TEo zH^vFPg!NbU)QE5bPkz2<{|=1CYrp$`vNO1feqmvFKvco^YRDL<4d^g;fJn7eT2B`{t*by9@-`UcNO0e9d1~pB1nsFA zC?kBcB&cx7$;n8x1~;j($e9hY1l%!9CQ>~CT#od-`uh7(K8-9fm4Q%>Xu7&Q7Ew3_ zxQ>DVE7ZTb`eCKj$)ymzS5w`hxhR8fCtu{O?(SFADJ@zQVEW`X4}@0~>`H zF3ol(K`$<~M`ntbVFB%&X{ypXUzrwEG3Z?!3NL`~>_XbWXdw9^2PdcDf%KKz12M?- zBJ)p`vS?9!zoBgUL+}qsG&T;-&##~ggABY_HYnT*2_2Bxit^Y#%o&qm!fXPici?2Y ze;-Ta@`Ud4;#eO};w&QVxy{Kk(3#riMg|84I*{Q!*m}Y3MgZiD;dDD(7!DbT85*W{ z*sFiB*d9vXGb$l__wJvGMi?x%1jaa=M?IY>D=jSs)b&-jmPA280aj@`?XLKJ3D7FL zy0AeG1mmq4EZD{6aAjwY0Zz@@u3$!Uorj!2q_pnFi#7MW?~*_Poy-D-iWSs079^&`4;Nv<8Ak zaCmrU2=RmOCB{-n1}rKnYT?SD1X1|oC|o?eNgI!Qx#X2pq@-`5EG&TW`U{FBl8(95 z!T_UzDiQz!CxcwPALt~-ty89Nt9Irasi6;HAi&qc09jN?pj>a1&{chfMqC@Iam;P5 z?zshrg{^$m>=ej;pw4o@i)j3jDg(-@7dsTREVqx58)<5fXml7=&B}2JkqjE}x(sO;p*_L0b!f z9f20%0s8-k?E@$__jW$WYQe zew-40pM@p#u?;BCgfJ42vIBnN^65i{17U_dO!_WBVn{0@?KBXs4>u>Hb=j@A(Gh@y z!JR|u@mm+*4CWt9j!Pjy9h};?JJ~ga-txkx9_lwV(?J&Lr-w6~4O6UN zv!CizaLQ>s>YD+(#@^A<7}9rbaP%_=H3?zB_7tm%{86L zc@^_CZ|@4bMX{G4p5nGLHn`xnruZy?lokpABRV4Dlieu_;E-&X-@afXy7SLVvKy2KcSgZy)paTI_%Y@4=g%GZlfV`!aX(n)ic?Ra-45vy z4d!d_=n!MPRXY5vs+N|qBCInVJjho$ zFnjsZ2sCLjUW;4j7Z));>kk1VLt%wbyH>4B!6oK#)bFu^e7hUDLqXjSj3DU(8*Q;8 zj3SwK9Y8&c=H$rjp9<0vAeWvFUPHjhKGmYp7D^C3*y<4y_%_p8~kZK-q zT{?(%gZ_pL==Q2>IR7(9prOIv^VnPel9mCiVAFDQqJ)u=(ZI7mCowM_;ta*#zIB3s zfCQ__BED2feAX)NGl?gYu6A}!fDe)O^&J5_1Qz}s1LFZ$_uq^3Mfhx?eetA#c*UeaK7+2qjj(#EwRia1DQuLZ$3f;1CZX% zhrD}aL+MZC{YQv`HU!(sXnUp_=W5$zw0y7u^tPFqr$ZVTlZP+?CB}}z=etk&W}%gW zi-v`mn40=IuJ79pnjeQ@3p!F70yF~&?e7)|&PJ#HJuMu#pdzg?dAxIlcgBYT##KI79UuIX%76ogkr&kL%9@r#QmVVy(eL9 zfh~zVR{*UA6P)&dgoNknqysnp4G2xuv+GkYFP>V{u;PZi8<8dHjrp1R)GB&z#$I^Of-XNj%152DS1qcx0njPf_t`me2lai0am;Y z4aLnmyDL6EnzNwbhjdwyJq(N}q=AdH$E#p=s*i|CNP-}LV+idCn}DDbpkE23{j<2u zJlxzC0Qqn7;KSE)@bE16eMK_qf$t}cm#5c}b{ONis`QMp_wafMIwmI9TLS8rWI={VW&~vB1F{0cVW`%%gWDisGYAW(KK22j ztadBrfo441QEBgQIni{R=b+NvM&=Wsy8@HgYZZ^=HY^*$ls#XjDhUG1$NG z3ou9EWV@Z2Gb{(9-?}AUXBSyNzC$qo_-(&4opxaR@qy4;o^{%*zPqUP4bVwaVj9UR^7}_s^Q{2s$X`S*nuWLRO?snwpvri826*1PRDtZHIwR zy#(1cMpo9qm>6O`??37$CMF2PBd1@VSS%q-4djDq=g;leV)K<#6BA8vUG|=BjtYZWL)RH;|Th)8Q!nWBqj7Hes~CtQqT9|5AzDMi|lyb1Z%w! z=^^+4OzemX7g1kWUg6cVDvzXm6xQQoSkKTC)j?0 zEdOgXpA>%QBhe$uJ9i=?BmIGd2?z>qRInlkC9P5q3|K($VxR@X6GucuC}m0ARaR{o z=IRelNVp9M16cq;3A~0x?9$S~0}F~TPM!YwdKjsRS~w8jKMRRFe0+RDF}3w)`q8KO zqRN`uurX1=W@UL%jtwCfsK8RMUghXn(^`Zkj_#s~i-#w^qHiVGEypE_3?SPU7r$Xt zb(2UYcu_%|o;FDEfAk_Q*RrjLM-6Qr?Ols~3ku@WpT8n3D#*pKQ-07^T*0*SR8eAH zbH)EFD}Lyg9&~Qam>NraH;S+|C0JSv*Y)n*Yn(Ihg6Z4r?*AXZXv+ZX*}%-~Eu15( z?BIg4VuAJb-u1uXsiV>pwQQ&O2$j`wk6*q7UFOl}F>~EUA4+Hwy*FP)UXrbfpBh*3 zbAx{O|Fn1I;Z*PIewAHPl(sDyLR3T}Ny$)1r7{miG)Rd=!zx3l$dIG~ZIwAgB&o zkRPI{@>C22&IN=G!5;u65Pq1kG*3x2V>IM%2tRV~rUIBwpj?7AE z^X}8RE@(zyK+}TH$Ruek_e*a)79Xg2*9L04k#PtXqme9JxjSHwSWR~Ti z3y3)raWq{MllTC`1Xli=H#?zH?uuQ2p^7d$7P~>5q*qUK9nV1L!jq^A0Q`BafvX#c z7)ujP%}d~)yzrJ}*7#{eWmQ_INUmJT2RjR&rKM%bYF7ksAmc2WIcpZKUCxVFuXu3d zc>y-XWMwNv)2*)-cj;D;xd5n3gt{1jEPlWP%Gg3Nu}IC`2p?e|S`(TX7?&cDDp%gU z(+iP7ULU}2IkYZ{Cs~f73c%<&2R5ExZ%quZs!%?}$-sot*wb^RslcnCpnwguhkI0% zMfTHsK*l9+G4F%c52cIATae8!pm0huXQWkd46!0o=m2nNxI;OWv|J;=wM6TVH+U^* z!8|KgtXPHoKf3Lyj#2z=dh?KN8$nGzc=Q0NAL3>eDj*~Vgc;x8MdlNHfQ1luLCmd~ zI(2GCiV|;Uf|eP6^IZI96du(uWtWGT=7m&T_oVCW)9kNaoaYI2Yb?|25L?V)TDV* zeATLlMfOQ{qMa4f#dh~!u$CPneI4^>+Sw5q+9YS0gAM{}VJ@bpJ9mT-HuwcKK2EO` z-VuWP(PMbAP6b@_{r_mrTCMB(0d#rW_Bh9|)hv@|r#6e3- z1i7ZbsF{%b2BM)SB4?mnm@8&ftB2tcyVOv7`yRS-^F==MbJ^KSq0T+&QUeJ0kk1Qa zp9R2QTWz{5U42X?hNcZTY{&bXH}42h;02CV#pBdhzC|Pt?-2x28mw;M3D{t~`mxV0 z;}43km7ndgx(qpDJE6)Bww1HN`NuqVnC?d4HLJ$JXgo7nUu$eUt>?_uyZQ)k80)qHMkj#T`;Q-0V&=)sfe)hyto zFt1HrU6(O7Gy$AxXKa(RHemDWZphvHIXn6I8#ianInTF5>pnI{JICLO0GivCdX(SX zp=p}XhSQv=ylOZ;KvsLZPmGOKHu&&JtaAQ(lw?>mHIlyp?kP8lnNg*^)v!D+zpkVt z-n>Ox*~}|$y-BXC5}EQr!Lesvs;K-#tu?4WrgA*T0F64Zcql93E)nUu6$pDRt{j8JM#`;V0bAI0LC2NbSO1 z%c?RqH@^b<<`dG%4tH+%t79}=%D%X-F}DRsc=N%7cV(7iUtrkTFFs#fF(S`jXD@0X z#Y?jR9>b*r@!vxXB;YnAS_C8l>{-#5hs|#j9pN@PfmK`2;C8we6lBz5pk-@ov%-U$ zxIVBaI*a2BrFbDLpgi3j{w#saBzaiC)E);50n~!ztuTq)T4BnGl`g-%n2UT%KD@rl z%F3vF6}z7*7yIRev5F$IvVtWw`^Bm-0eE6!JaxnOjLTx>RYY| zod;b3!80-d%MQ;2?K4DS_5%e%tEez5u0HYGRv_n&fTYedKO^)=askGRLR1NoCLwv; zuR5)PBr>idaB4pOyuaSL*KkDqzV^pk?qEg5Q(Z_&vqzx$h`OVa7r>1-7VX;`8-ld zQ8?X2o(9DTBUVI&_WQMk`WY$O$!AWVu5z@6?0wi(OY2Ih4DK>3_(%6~{cHCr9mAA$ z30z@QclUD5?b`)wdtwg3aUiyKtr65Qd;5pz!s*a#=AkD=rlswIfaVQqOfBH%%~=4| zF#RMw9s@!3+=GM1gIjJ=>!_{mMQ+u57Pv(UOsuFmQ_`C#lVY+QTB*KeHrl#=P4cR$kVIhW{A59be1x*tdcLs(L2#GMR=2ul2?a6gD!~4RA z#togP+BTeduk&%uE_CGN8W$HFrlz*GIN6<1<&rD$J4;@@dJc(k^X}cjk8*qJv!$Ux zsDlK>b>oBF(v0^lCM~@kQ@X$+V?mbuFu1bzEJa1dQ>{<&f2&%+T7{2Mv=Y-m`)h42 zts*qHWY`bEqVqp!+Ub#yknm;i3(({^u+4$S=6SH^fa{(PDnDzqBY^_p=->3F9-oCb zr~ji}Zu^de_A}R!riO5Ss<+&J&_W|e?f{bVS+Dg+bEG%H^U}M1Hpkrb(lL40YJk%5l6PK0g&@8O2GQQ^A==iaY z(fe!HqO1PIuvD>v+ilic#evs1tn@+z4AX2@{&ZyTTjlRZvxAa4r<4$XinjX@?a_11 zOiKCiLRRx+sD`+dQrq^Z^2)T!CW+H^hE^jm9EA>6b_^9u?c%*OHd$B z#K$CC88l7Aq3_Lxyo)2ae)a-|!MnEy-e-@r-UeQSmaPgYhoSh*=nnGDxB-i6ahm$! zFQs^&!YdKwv-9n;`aR7%AEl;xLI9-Z850{5QEr(H%G3MP3! zpn6(6@Z#mmJfi&ZE|WF0xsKf4L0!MO`2c(w&A1xW+{rBl^=qkqOd}%0@@=Ce?||W2 zh+hgwNk~2ObT{M0WM(kJp75N^Mc)FsPBLfi_*X$3uZg?2jS>$zMs@x(lF%zPTg(2p ziO#m*Zxfy5=Ur3w)utU^B`5d%k>x9JkzTMvh{1G)cI=cyR2vKr8aNQ}hU~{c1f&k3 zIHt&wyup;4hjN454QkeNh#RZnEpkuYIj^;~6%o8b`G&LZSZWX%7dHdg+zatd=2ELbiB&P&g!oT~CjhWq<#yxI!cH%eHbS z80+o&^fP>(cyQ|^uoQ-ECr3Mz!1)w|oDAFd%6m}%FK`A(eJsH!A<#`aS5s3{;7-Du zFSj?ynd6kMJJTZsyBj6?DSCxJLCn$Z-A-|9z4<+raU^a7Xi+Dj4}9bZ2e_z^3K?|+ zc^WEZ!i!Fr#O$tHuBKkOXVw2WRfc+B@^=&$E3o*7+1FV%!lmOZymqyuK|~Gv>*Flxk5dc4j}HlTxRm1kFfk~p(?EW2p~ZYS;q75zMNy%PMCyZ zMfOnKvS!@w8JwJj;6Vq4-7}jjx)4cyXjLwTBio1sr%@I0T4xa-C%siGRM7{Nn9`%} zMPi=LkHtKm0b2#KFHT@tV`OT^V^VBtmpzT)&>nsB>Wv%jv`(eh-Z+8vt?Thyl4%&v zz{=pLCOy6aB_b_7eF|e@v^p1!lMfAU&ES%s%O{(T1Rv56?AkaUYP9#}t_G*_&;K>s@q-u1=0_x77+YD(uzv zTb}e(KYYkd;2OczQLh^*Sj0Qh4x<~=@&vzl^JW?Ydjh20cHo{7 z5ze5^)X88nAo&!ayhG{2>|K?hm54HBLBY2ZC-M<|dx#<3>1p4mI8o@y~Zm?mfCZb)_QA`aq5D&Uz5oVaxmL6-^jo-vv-0`=K{{`}WD79H^~l(Ok$eXT*V z?Dt64cEFYx{ho@u1;`+(LD$GAMrb9B;JlkRZ^oI|fmPiXX4vCH9fsEe0~gE4%5q&# zQ2bnf86yAwwz7!%AX*?S_4T?<3ewEZ?3{=46L10`MB!CBCO|2vnGE~x8ad?CjFwg) zeGbQM=Z?;@2=_0KVZp~ZVr=srxDjjqJTPdAZrC75URmJ&q4)tHTi{|ZFl#$@PCDZb4YyK(*1eZee&r5g0t?pQa$s#0 zi8b)p_E(wO!BMlTca(S$2rux^MFNbJ%wqI3Nx zXX4GG4{S@}vz_t`2vJyA7zl9-h*NN>L-oRuo(J&k0t4YkH-Z_$$m<7cox|LkAY@`~(hc--3sa*fbPD zvdcpsBcSB#tA?fd6k_0&Hfl2q!*mNqf)ZqSD8%6+L8qhO2*li>W0`OhVG}ql!U9u8 zhEd4GDUU*NqxT`mxK&Gwazl`#160IV{3xQx;JQPAUVu`@a(4C>xD;!7%LfRn3H(Jd zbZjb2Mtlin|wc@S8&1!2f(zKhsbd6 zS1RftY{~TFG}v~Y=6TFAh%psm)Gtdvd6*6p)bNVTQh(*q8Tj0E2O(OOVPqm6p}2TU zZ>k_a|3Y9-SPQTq;R0gK1l9Ou4>1^jLUo-0VuZ%n2&+r(Sj$Q$yIHzw{2bk^7_WfP z&@0vE&WU+k_Yyv?b(T?f>XNf|K!u(cJa7?3ox(E*LRmFu3`i4YSG-jHfv0WxZRH8x3`W<>01Y`_LKB33DY2Nhib zo@H436gDOrsU;dkLC?xdud&j4MWvaHBuEOw?G`v(W2|48ta0lp-Nnq_95qMB_4Q7j ziCZ3B!ANi!7aBn{y3p`Zc+OE7`_XC-9{f#wcdXG##T?K^9*C$IrfWqrVdbV)L4Ito z#Swb<`i65S%{~j1LU`@R81V_jZ4qAL9C*R~FchRY4#aHwNbhOa?%h+)Jvv`AtiCf4 zCOx9LFq|q5RqFd6T!4nM1(%jvSolHh)cEXQ6^}QoXH8Cg_dl5ZhK0FMgf>^usR8y* zcs*I2J24FoeOidkXvDWdAcJWEc<`&DqYOh7ObecDDkMZT!>EH|L`)bi`08Dzf^X!{ zxU^SvEB&1tGdJt#>^Na=mc6mNuEnO~z~rireQEFp`Ajn9!%;j?)7WvooEHtvTKMqr zTVarAn?BdOGDt$hmr#(LvHV{}kNDRWhugd^6(3|4pRp}IW?P^-|G*1&7tfGY9^hVC z4o->x$Xn$Olx_g)Yghu#TBtW`;EEQ%yu3UZ#n%u0Y^_#GstxNTz88G>2#va_r)LFp z!a|(pLYx;5#Lwv`gHj$p<^fqm0es$(krC>EM9kp6`2fpY!rVr?w7_mIgK8YN%-A%k zu%m-l)f-jT2NmTKE}<`qPXC9)VI0tQ&@3U4AZNL`{mm`|gTbPDtzLXu}1~bkRr6hE!&&0R8H37!!2 zma=BW^D@HY;Z<6R2P;0a%40Lnwz$92F&p+d$6j2GU2Op$mrE-kegT_6 zym9e8uLYx*?2 zKlD4&w(Wbd@>_qPw-o{15+mfy6OBMK#!c0&841_&g=}D z!ND;Xy^?stg{dQ)_U7c9lWJX zZtB}GZdCv|Ul;^Akj){c7J;ulNuykeskY-N!_Vn9xj4cjDfKD67kNzhuQoym@uhkI zEy>;gbG?5OWCLM<1C-#Y)=t?qs@N1lEEn!@Gq62cE+~AZRaRx;Qr|~WLxEM}WV+N; zRWE3{%=y@z71Ok!qa#MEa?e(gp25AK)M?WTS~}EVAD|cu%)uqLb{GfYReOe?C;AC7 zG_h5ybUv?r_vn!fDK2o_ZG3vHE+DxH|2-3Pqm;$%mEiM{b0M`wdE%P|rBeZpoKlAI zs@Nb}03RQ%`2gx$W}Y?wnS1iODr^#M%=3=KI1`_usQb90O!|>$j;W_GS9cjZZ}01J zt2I=&Ab7KEtUR!&fMcP@I_IzVsX)Mm$D=yqIHF)uW>u)h%ns9B*K7&_X$4;z>^^N? z_k|fK;Ux4#ZH|?f2=v^f6Hi|Q;V?Z#AY{mXtCJaPt>0ngr3~Dv7lTH;A9(_Yri*;m z3j~^;Hn>&ri$v`^3$mk_EDW%2z`}9~j@3KD}WMc5q@;wiep*eSZnh%gasm zJG>_|m9{Guh9r4*6(yEibr`y0*$ya$i+Dnzo%X$To2UqH@j3Y1-fjsz%D#YV?RQi_ zy}Zw0N*}9D>wn>?5qIo8d-tybT{@WJ4<#Vr zwIfDZoL?Xhj(S?aiFGTHoZsP2nT8RJ!PSXZ>7FpBS5d#DyKtKd^c)Q0$d%6`Jr*P$ z4>=f5l%D-FqHs=%GEsrkrtQbpg0xfZKT|RTOd+W@&)n_m|Ahhzboayu~8=IRiAcP9KkNk3sbCAl&84d>33kGcLcDfc4lKA*2atBz>HUm`< zYk6iOgHm z|HyI{5VJevzI?bmONUtM&LuM$z#K&U!x-Oz@3yE-rzJ-_Zm;FC%O09X;d`oKph535 zv(^1Q{$^yk78<$&bYeotFytkUKuxUoR!uaT1yFe$KI{;gzgkkbj(NQnE>^C3fXQ#m z;otdM;8=lPMyLy$pL0uP8eE1$+F(D>OZs})b_5()^nHz}x#i{MVs!n63~T4`b|xN$ z^`?1zxPcG&(W&=lSJzQsMrmY~A^t|*y{nFtWOc^dt;(RV9PhELbgiBs!@h*t+RNO1ay~TyPw)Hmqkn`@2geC2jj2HjEIlhOE*>(eTbZit z(uJ%Kyn64m9A)t`0YgaYxzui zS5WH9X5AhOKMi*P1p1_XgM%KvzO(SctB$-|#F%ucKloflsHP=eU*FNC+8EzTC`4lq zEt`WGktH?IS+nwYPC-793r>l*UtASnrZuD;XTxJcHEjw5$nZQKHYf|_pnaliF=qQ< zCY0{|m|`>h{$q`SQisc(kPtt}USwE?-o`iwhEsCj@4PoBtTMr&w{Dr(y5IwBhCjp} z#2|P~Hb$kj9OP%BZ-3m+oEoUV7U+TYr%n}uAdQpN4jacli3>QH8>96$Me9X?)5ElM zFs*{8Jgz4w9r*(^nDYSSlr)0cuYo%O3Tq03k|`JvLCBuNnkMW#5S5eT1#60M!zZ3& zE){)g!^wZ-uF+||{p+s?tP5@rTDA$Rw23Og@;%TfnKh#@*{nMBdOish$aIir8f$N` zoZa_usACKswL{39;ZF;(;yYlRc2#5#zk-&w3L)V|5)zab;^5#Ii`=BWBWkpD0%Ri_ zgJ^xsz~q<%7Y63&&$x!q+t#GNbgcdU9>yBf6g8N^Y2yoF90aR=mA4DkJM?GTSB-+p z*|QJ?(I4l9hleu|3s{mfIpIut@5YeMgl(uHj7AjRieOjdhO@uof|@l#)`M*-0)HPi zVD$UMVP$cdpTu=5UT#;4j*b14Qf)44;OxCs8?6H5$GhM|*pr7X2aVvy3|CCAj)sUv z&@Rc4{~Y&JVx~~j5Vu(9p<@o5MJt0~qhj#z@Bmjbz`Icz1V;k@K-;De*td}pxpgF< zpAm~!=^h^u@SBdQjnoi^C;(hi%X&`trmnh7KG+6X^%3%*qXPm17ouvz6kiCb6&tfJ zgHw>Ugl*J1Yj=2#0Bb$H7G!iV2~F_hVf5tzSDg&j(213|f4;y2q}@OyBz+yUgls-r zMEUailda)o@2W}^1TET!Pq)+(wqW|U&X!hB&~7~2ihxtJm4*UJpVWBnj63Gw^?UD1 zG^f{;a$c+@02&j7jBVJe^u6mb&}tDHU*wL2F?JTdXBGQUbVl#2`DJns&oMe?=Q%0+E3Jg@!$)WfXj&zjfsBwRo-aHHsWtr`Chobl#W)J_jeTNbIG4@AUf30!2rS@gKZNhLZGu8Vmy>cv<2AW$ z)(9XmMi5C;^<{$RD4Dr$qZW^bYxaJMx(kujrf!zIcyV!XJ zA#pwx^(~R*R+LgC-z}Je7mYWeq30xm`Orw`gc&{6ek%w^(|GJ}gxqkIS4wIVj)oa&2 zczizx?=55Y5!D&Ea?Oaakt40+K1sf`YznZwEZ1#v%jhp_%ykxDzl{DOqhs=XKM#0< z*epgHr$mG$>@djV&jV+xP=V~sgRwmg3YMqgaih&07%|}FKaWMx?}jM~iR-KR0doY_ zu|F#<)%yH(OCrp-3ne9A>^uL~Uc$QhNx>peyz~VCli!L}%3`%e7(*gj`Jix7uZpl> z>o@|LC{T^qV^D^IgFJ=gPq@oo#?ujmhnm-(Lrnd6k85zv#$}18re;HXN!DL|FsOZ0 z8o~CIJn=lW$=}ykzf^we91U?SYLONb7Y{sw14d{wU>GrI@~*)thpqf1-dLL37H?#B zSa<#t>}+H4Z`QyLs(hk&aif~x%pltZ7B_~vj*2;O|kLfrX>S?HXp~I5=e3 zyuT;?X9PuP4FkYw;0W$GevEu>0d^C3glS;LOFl&0FKf8o*)YSX zscqwWKXYFtw|aGXd;87n*QY``Z5an4O+q#D&5=)@7_2vBuXN$8j7|JQgZ7-1z1{*B zJwuS-QC^6)~EywVWZ@x-D^RD!rH3lnDlmaxSi?afAnxm^5uAU z`0^Pf=$guRlngrNyFj513>#`UG7}j>nkadJ_ASlmcaFWy&0lmrnf@oall9XFZ4)5K z;4xi&_M5$fgF`qrlmKttA9h`6y$lAf&I&&Phct=W>R3~z%bKAT@433Uj2pVoV6iG- z4Q8N=VEbtTLFrb9mkS@2sr9nbsJcLijhVs(HGnU|P{WQWq6V<8=u1;o?&h0TVJlbmfo@+q{=666y8 z0Z)RqBU%u0>%InaQ%dTwN?xVcJUu_I@6O~|sgSP-#6jnsMn0)z3wF3M8oRsAqVNAs zyHco&6gmCf&(9C}>6J*;OUAxHI{%+@EV#%=I)8`zShIiGTxqZ--mZtqjH;56mv@Vk z0MVS7p?u4P@8}=05g~SuO;Et8H-pjwWw_7A#usHyKF%~V2sTn$p?mhEysRwF8k+|< zHU(DOn07dGRpj(I)7j8sn#hb zpCfA^1kFS|2C1(;j?~NE2X$BLJ)iVj9_4amWixO<9Lu_;p(F{S!Sv78gkIy4P37jw z@x76%{g8Oe`5$mdZwZ`L%10^*u~hSC@%9>+j4Q zcru(-9rLB^w>eRHa@aOksd!ip%s!v0^l=9a6$8|Dd*rODG{)ZE8NB@WKyTje-Ac+pMMjKy}}R{{obZV%Y!y literal 33091 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 b&+w?HVn#_}r;P0J!5I4MjPx>eb{_pd+m8tJ diff --git a/playwright/e2e-vrt/layout/Navigation.spec.ts-snapshots/Navigation-App-Page-With-Side-Bar-Shown-Mobile-1-chromium-linux.png b/playwright/e2e-vrt/layout/Navigation.spec.ts-snapshots/Navigation-App-Page-With-Side-Bar-Shown-Mobile-1-chromium-linux.png index 2ce1d7971c1e17b2579d8fdb39b5e9fa561dd03c..ed5bf402c776b7be711ef0f89a6e6c3ef05f1d0e 100644 GIT binary patch literal 36759 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 z

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 D7)mh^ diff --git a/posthog/hogql_queries/legacy_compatibility/filter_to_query.py b/posthog/hogql_queries/legacy_compatibility/filter_to_query.py new file mode 100644 index 0000000000000..3602dbcc538cd --- /dev/null +++ b/posthog/hogql_queries/legacy_compatibility/filter_to_query.py @@ -0,0 +1,269 @@ +from posthog.models.entity.entity import Entity as BackendEntity +from posthog.models.filters import AnyInsightFilter +from posthog.models.filters.filter import Filter as LegacyFilter +from posthog.models.filters.path_filter import PathFilter as LegacyPathFilter +from posthog.models.filters.retention_filter import RetentionFilter as LegacyRetentionFilter +from posthog.models.filters.stickiness_filter import StickinessFilter as LegacyStickinessFilter +from posthog.schema import ( + ActionsNode, + BreakdownFilter, + DateRange, + EventsNode, + FunnelExclusion, + FunnelsFilter, + FunnelsQuery, + LifecycleFilter, + LifecycleQuery, + PathsFilter, + PathsQuery, + PropertyGroupFilter, + RetentionFilter, + RetentionQuery, + StickinessFilter, + StickinessQuery, + TrendsFilter, + TrendsQuery, +) +from posthog.types import InsightQueryNode + + +def entity_to_node(entity: BackendEntity) -> EventsNode | ActionsNode: + shared = { + "name": entity.name, + "custom_name": entity.custom_name, + "properties": entity._data.get("properties", None), + "math": entity.math, + "math_property": entity.math_property, + "math_hogql": entity.math_hogql, + "math_group_type_index": entity.math_group_type_index, + } + + if entity.type == "actions": + return ActionsNode(id=entity.id, **shared) + else: + return EventsNode(event=entity.id, **shared) + + +def to_base_entity_dict(entity: BackendEntity): + return { + "type": entity.type, + "id": entity.id, + "name": entity.name, + "custom_name": entity.custom_name, + "order": entity.order, + } + + +insight_to_query_type = { + "TRENDS": TrendsQuery, + "FUNNELS": FunnelsQuery, + "RETENTION": RetentionQuery, + "PATHS": PathsQuery, + "LIFECYCLE": LifecycleQuery, + "STICKINESS": StickinessQuery, +} + + +def _date_range(filter: AnyInsightFilter): + return {"dateRange": DateRange(**filter.date_to_dict())} + + +def _interval(filter: AnyInsightFilter): + if filter.insight == "RETENTION" or filter.insight == "PATHS": + return {} + return {"interval": filter.interval} + + +def _series(filter: AnyInsightFilter): + if filter.insight == "RETENTION" or filter.insight == "PATHS": + return {} + return {"series": map(entity_to_node, filter.entities)} + + +def _sampling_factor(filter: AnyInsightFilter): + return {"samplingFactor": filter.sampling_factor} + + +def _filter_test_accounts(filter: AnyInsightFilter): + return {"filterTestAccounts": filter.filter_test_accounts} + + +def _properties(filter: AnyInsightFilter): + raw_properties = filter._data.get("properties", None) + if raw_properties is None or len(raw_properties) == 0: + return {} + elif isinstance(raw_properties, list): + raw_properties = {"type": "AND", "values": [{"type": "AND", "values": raw_properties}]} + return {"properties": PropertyGroupFilter(**raw_properties)} + else: + return {"properties": PropertyGroupFilter(**raw_properties)} + + +def _breakdown_filter(filter: AnyInsightFilter): + if filter.insight != "TRENDS" and filter.insight != "FUNNELS": + return {} + + breakdownFilter = { + "breakdown_type": filter.breakdown_type, + "breakdown": filter.breakdown, + "breakdown_normalize_url": filter.breakdown_normalize_url, + "breakdown_group_type_index": filter.breakdown_group_type_index, + "breakdown_histogram_bin_count": filter.breakdown_histogram_bin_count if filter.insight == "TRENDS" else None, + } + + if filter.breakdowns is not None: + if len(filter.breakdowns) == 1: + breakdownFilter["breakdown_type"] = filter.breakdowns[0].get("type", None) + breakdownFilter["breakdown"] = filter.breakdowns[0].get("property", None) + else: + raise Exception("Could not convert multi-breakdown property `breakdowns` - found more than one breakdown") + + if breakdownFilter["breakdown"] is not None and breakdownFilter["breakdown_type"] is None: + breakdownFilter["breakdown_type"] = "event" + + return {"breakdown": BreakdownFilter(**breakdownFilter)} + + +def _group_aggregation_filter(filter: AnyInsightFilter): + if isinstance(filter, LegacyStickinessFilter): + return {} + return {"aggregation_group_type_index": filter.aggregation_group_type_index} + + +def _insight_filter(filter: AnyInsightFilter): + if filter.insight == "TRENDS" and isinstance(filter, LegacyFilter): + return { + "trendsFilter": TrendsFilter( + smoothing_intervals=filter.smoothing_intervals, + # show_legend=filter.show_legend, + # hidden_legend_indexes=cleanHiddenLegendIndexes(filter.hidden_legend_keys), + compare=filter.compare, + aggregation_axis_format=filter.aggregation_axis_format, + aggregation_axis_prefix=filter.aggregation_axis_prefix, + aggregation_axis_postfix=filter.aggregation_axis_postfix, + formula=filter.formula, + shown_as=filter.shown_as, + display=filter.display, + # show_values_on_series=filter.show_values_on_series, + # show_percent_stack_view=filter.show_percent_stack_view, + ) + } + elif filter.insight == "FUNNELS" and isinstance(filter, LegacyFilter): + return { + "funnelsFilter": FunnelsFilter( + funnel_viz_type=filter.funnel_viz_type, + funnel_order_type=filter.funnel_order_type, + funnel_from_step=filter.funnel_from_step, + funnel_to_step=filter.funnel_to_step, + funnel_window_interval_unit=filter.funnel_window_interval_unit, + funnel_window_interval=filter.funnel_window_interval, + # funnel_step_reference=filter.funnel_step_reference, + # funnel_step_breakdown=filter.funnel_step_breakdown, + breakdown_attribution_type=filter.breakdown_attribution_type, + breakdown_attribution_value=filter.breakdown_attribution_value, + bin_count=filter.bin_count, + exclusions=[ + FunnelExclusion( + **to_base_entity_dict(entity), + funnel_from_step=entity.funnel_from_step, + funnel_to_step=entity.funnel_to_step, + ) + for entity in filter.exclusions + ], + funnel_custom_steps=filter.funnel_custom_steps, + # funnel_advanced=filter.funnel_advanced, + layout=filter.layout, + funnel_step=filter.funnel_step, + entrance_period_start=filter.entrance_period_start, + drop_off=filter.drop_off, + # hidden_legend_breakdowns: cleanHiddenLegendSeries(filters.hidden_legend_keys), + funnel_aggregate_by_hogql=filter.funnel_aggregate_by_hogql, + # funnel_correlation_person_entity=filter.funnel_correlation_person_entity, + # funnel_correlation_person_converted=filter.funnel_correlation_person_converted, + ), + } + elif filter.insight == "RETENTION" and isinstance(filter, LegacyRetentionFilter): + return { + "retentionFilter": RetentionFilter( + retention_type=filter.retention_type, + # retention_reference=filter.retention_reference, + total_intervals=filter.total_intervals, + returning_entity=to_base_entity_dict(filter.returning_entity), + target_entity=to_base_entity_dict(filter.target_entity), + period=filter.period, + ) + } + elif filter.insight == "PATHS" and isinstance(filter, LegacyPathFilter): + return { + "pathsFilter": PathsFilter( + # path_type=filter.path_type, # legacy + paths_hogql_expression=filter.paths_hogql_expression, + include_event_types=filter._data.get("include_event_types"), + start_point=filter.start_point, + end_point=filter.end_point, + path_groupings=filter.path_groupings, + exclude_events=filter.exclude_events, + step_limit=filter.step_limit, + path_replacements=filter.path_replacements, + local_path_cleaning_filters=filter.local_path_cleaning_filters, + edge_limit=filter.edge_limit, + min_edge_weight=filter.min_edge_weight, + max_edge_weight=filter.max_edge_weight, + funnel_paths=filter.funnel_paths, + funnel_filter=filter._data.get("funnel_filter"), + ) + } + elif filter.insight == "LIFECYCLE": + return { + "lifecycleFilter": LifecycleFilter( + shown_as=filter.shown_as, + # toggledLifecycles=filter.toggledLifecycles, + # show_values_on_series=filter.show_values_on_series, + ) + } + elif filter.insight == "STICKINESS" and isinstance(filter, LegacyStickinessFilter): + return { + "stickinessFilter": StickinessFilter( + compare=filter.compare, + shown_as=filter.shown_as, + # show_legend=filter.show_legend, + # hidden_legend_indexes: cleanHiddenLegendIndexes(filters.hidden_legend_keys), + # show_values_on_series=filter.show_values_on_series, + ) + } + else: + raise Exception(f"Invalid insight type {filter.insight}.") + + +def filter_to_query(filter: AnyInsightFilter) -> InsightQueryNode: + if (filter.insight == "TRENDS" or filter.insight == "FUNNELS" or filter.insight == "LIFECYCLE") and isinstance( + filter, LegacyFilter + ): + matching_filter_type = True + elif filter.insight == "RETENTION" and isinstance(filter, LegacyRetentionFilter): + matching_filter_type = True + elif filter.insight == "PATHS" and isinstance(filter, LegacyPathFilter): + matching_filter_type = True + elif filter.insight == "STICKINESS" and isinstance(filter, LegacyStickinessFilter): + matching_filter_type = True + else: + matching_filter_type = False + + if not matching_filter_type: + raise Exception(f"Filter type {type(filter)} does not match insight type {filter.insight}") + + Query = insight_to_query_type[filter.insight] + + data = { + **_date_range(filter), + **_interval(filter), + **_series(filter), + **_sampling_factor(filter), + **_filter_test_accounts(filter), + **_properties(filter), + **_breakdown_filter(filter), + **_group_aggregation_filter(filter), + **_insight_filter(filter), + } + + return Query(**data) diff --git a/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py b/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py new file mode 100644 index 0000000000000..6f1fe48d02c8a --- /dev/null +++ b/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py @@ -0,0 +1,1008 @@ +import pytest +from posthog.hogql_queries.legacy_compatibility.filter_to_query import filter_to_query +from posthog.models.filters.filter import Filter as LegacyFilter +from posthog.models.filters.path_filter import PathFilter as LegacyPathFilter +from posthog.models.filters.retention_filter import RetentionFilter as LegacyRetentionFilter +from posthog.models.filters.stickiness_filter import StickinessFilter as LegacyStickinessFilter +from posthog.schema import ( + ActionsNode, + AggregationAxisFormat, + BaseMathType, + BreakdownAttributionType, + BreakdownFilter, + BreakdownType, + ChartDisplayType, + CohortPropertyFilter, + CountPerActorMathType, + ElementPropertyFilter, + EntityType, + EventPropertyFilter, + EventsNode, + FunnelConversionWindowTimeUnit, + FunnelExclusion, + FunnelPathType, + FunnelVizType, + GroupPropertyFilter, + HogQLPropertyFilter, + Key, + PathCleaningFilter, + PathType, + PersonPropertyFilter, + PropertyMathType, + PropertyOperator, + RetentionPeriod, + RetentionType, + SessionPropertyFilter, + ShownAsValue, + StepOrderValue, + TrendsFilter, + FunnelsFilter, + RetentionFilter, + PathsFilter, + StickinessFilter, + LifecycleFilter, +) +from posthog.test.base import BaseTest + + +insight_0 = { + "events": [{"id": "signed_up", "type": "events", "order": 0}], + "actions": [], + "display": "ActionsLineGraph", + "insight": "TRENDS", + "interval": "week", + "date_from": "-8w", +} +insight_1 = { + "events": [{"id": "signed_up", "type": "events", "order": 0}], + "actions": [], + "display": "WorldMap", + "insight": "TRENDS", + "breakdown": "$geoip_country_code", + "date_from": "-1m", + "breakdown_type": "event", +} +insight_2 = { + "events": [ + {"id": "signed_up", "name": "signed_up", "type": "events", "order": 2, "custom_name": "Signed up"}, + {"id": "upgraded_plan", "name": "upgraded_plan", "type": "events", "order": 4, "custom_name": "Upgraded plan"}, + ], + "actions": [{"id": 1, "name": "Interacted with file", "type": "actions", "order": 3}], + "display": "FunnelViz", + "insight": "FUNNELS", + "interval": "day", + "date_from": "-1m", + "funnel_viz_type": "steps", + "filter_test_accounts": True, +} +insight_3 = { + "period": "Week", + "display": "ActionsTable", + "insight": "RETENTION", + "properties": { + "type": "AND", + "values": [ + {"type": "AND", "values": [{"key": "email", "type": "person", "value": "is_set", "operator": "is_set"}]} + ], + }, + "target_entity": {"id": "signed_up", "name": "signed_up", "type": "events", "order": 0}, + "retention_type": "retention_first_time", + "total_intervals": 9, + "returning_entity": {"id": 1, "name": "Interacted with file", "type": "actions", "order": 0}, +} +insight_4 = { + "events": [], + "actions": [{"id": 1, "math": "total", "name": "Interacted with file", "type": "actions", "order": 0}], + "compare": False, + "display": "ActionsLineGraph", + "insight": "LIFECYCLE", + "interval": "day", + "shown_as": "Lifecycle", + "date_from": "-8w", + "new_entity": [], + "properties": [], + "filter_test_accounts": True, +} +insight_5 = { + "events": [ + { + "id": "uploaded_file", + "math": "sum", + "name": "uploaded_file", + "type": "events", + "order": 0, + "custom_name": "Uploaded bytes", + "math_property": "file_size_b", + }, + { + "id": "deleted_file", + "math": "sum", + "name": "deleted_file", + "type": "events", + "order": 1, + "custom_name": "Deleted bytes", + "math_property": "file_size_b", + }, + ], + "actions": [], + "display": "ActionsLineGraph", + "insight": "TRENDS", + "interval": "week", + "date_from": "-8w", + "new_entity": [], + "properties": [], + "filter_test_accounts": True, +} +insight_6 = { + "events": [{"id": "paid_bill", "math": "sum", "type": "events", "order": 0, "math_property": "amount_usd"}], + "actions": [], + "display": "ActionsLineGraph", + "insight": "TRENDS", + "interval": "month", + "date_from": "-6m", +} +insight_7 = { + "events": [ + { + "id": "paid_bill", + "math": "unique_group", + "name": "paid_bill", + "type": "events", + "order": 0, + "math_group_type_index": 0, + } + ], + "actions": [], + "compare": True, + "date_to": None, + "display": "BoldNumber", + "insight": "TRENDS", + "interval": "day", + "date_from": "-30d", + "properties": [], + "filter_test_accounts": True, +} +insight_8 = { + "events": [{"id": "$pageview", "math": "total", "type": "events", "order": 0}], + "actions": [], + "display": "ActionsTable", + "insight": "TRENDS", + "interval": "day", + "breakdown": "$current_url", + "date_from": "-6m", + "new_entity": [], + "properties": { + "type": "AND", + "values": [ + { + "type": "AND", + "values": [{"key": "$current_url", "type": "event", "value": "/files/", "operator": "not_icontains"}], + } + ], + }, + "breakdown_type": "event", +} +insight_9 = { + "events": [ + { + "id": "$pageview", + "name": "$pageview", + "type": "events", + "order": 0, + "properties": [ + {"key": "$current_url", "type": "event", "value": "https://hedgebox.net/", "operator": "exact"} + ], + "custom_name": "Viewed homepage", + }, + { + "id": "$pageview", + "name": "$pageview", + "type": "events", + "order": 1, + "properties": [ + {"key": "$current_url", "type": "event", "value": "https://hedgebox.net/signup/", "operator": "regex"} + ], + "custom_name": "Viewed signup page", + }, + {"id": "signed_up", "name": "signed_up", "type": "events", "order": 2, "custom_name": "Signed up"}, + ], + "actions": [], + "display": "FunnelViz", + "insight": "FUNNELS", + "interval": "day", + "date_from": "-1m", + "funnel_viz_type": "steps", + "filter_test_accounts": True, +} +insight_10 = { + "date_to": None, + "insight": "PATHS", + "date_from": "-30d", + "edge_limit": 50, + "properties": {"type": "AND", "values": []}, + "step_limit": 5, + "start_point": "https://hedgebox.net/", + "funnel_filter": {}, + "exclude_events": [], + "path_groupings": ["/files/*"], + "include_event_types": ["$pageview"], + "local_path_cleaning_filters": [], +} +insight_11 = { + "events": [ + {"id": "uploaded_file", "type": "events", "order": 0}, + {"id": "deleted_file", "type": "events", "order": 2}, + {"id": "downloaded_file", "type": "events", "order": 1}, + ], + "actions": [], + "display": "ActionsLineGraph", + "insight": "TRENDS", + "interval": "day", + "date_from": "-30d", +} +insight_12 = { + "events": [{"id": "$pageview", "math": "dau", "type": "events"}], + "display": "ActionsLineGraph", + "insight": "TRENDS", + "interval": "day", + "date_from": "-30d", + "filter_test_accounts": True, +} +insight_13 = { + "events": [{"id": "$pageview", "math": "dau", "type": "events"}], + "display": "ActionsLineGraph", + "insight": "TRENDS", + "interval": "week", + "date_from": "-90d", + "filter_test_accounts": True, +} +insight_14 = { + "period": "Week", + "insight": "RETENTION", + "target_entity": {"id": "$pageview", "type": "events"}, + "retention_type": "retention_first_time", + "returning_entity": {"id": "$pageview", "type": "events"}, + "filter_test_accounts": True, +} +insight_15 = { + "events": [{"id": "$pageview", "type": "events"}], + "insight": "LIFECYCLE", + "interval": "week", + "shown_as": "Lifecycle", + "date_from": "-30d", + "entity_type": "events", + "filter_test_accounts": True, +} +insight_16 = { + "events": [{"id": "$pageview", "math": "dau", "type": "events"}], + "display": "ActionsBarValue", + "insight": "TRENDS", + "interval": "day", + "breakdown": "$referring_domain", + "date_from": "-14d", + "breakdown_type": "event", + "filter_test_accounts": True, +} +insight_17 = { + "events": [ + {"id": "$pageview", "type": "events", "order": 0, "custom_name": "First page view"}, + {"id": "$pageview", "type": "events", "order": 1, "custom_name": "Second page view"}, + {"id": "$pageview", "type": "events", "order": 2, "custom_name": "Third page view"}, + ], + "layout": "horizontal", + "display": "FunnelViz", + "insight": "FUNNELS", + "interval": "day", + "breakdown": "$browser", + "exclusions": [], + "breakdown_type": "event", + "funnel_viz_type": "steps", + "filter_test_accounts": True, +} + +test_insights = [ + insight_0, + insight_1, + insight_2, + insight_3, + insight_4, + insight_5, + insight_6, + insight_7, + insight_8, + insight_9, + insight_10, + insight_11, + insight_12, + insight_13, + insight_14, + insight_15, + insight_16, + insight_17, +] + + +@pytest.mark.parametrize("insight", test_insights) +def test_base_insights(insight): + """smoke test (i.e. filter_to_query should not throw) for real world insights""" + if insight.get("insight") == "RETENTION": + filter = LegacyRetentionFilter(data=insight) + elif insight.get("insight") == "PATHS": + filter = LegacyPathFilter(data=insight) + elif insight.get("insight") == "STICKINESS": + filter = LegacyStickinessFilter(data=insight) + else: + filter = LegacyFilter(data=insight) + filter_to_query(filter) + + +properties_0 = [] +properties_1 = [{"key": "account_id", "type": "event", "value": ["some_id"], "operator": "exact"}] +properties_2 = [ + {"key": "account_id", "type": "event", "value": ["some_id"], "operator": "exact"}, + {"key": "$current_url", "type": "event", "value": "/path", "operator": "not_icontains"}, +] +properties_3 = {} +properties_4 = {"type": "AND", "values": []} +properties_5 = {"type": "AND", "values": [{"type": "AND", "values": []}]} +properties_6 = { + "type": "AND", + "values": [ + { + "type": "AND", + "values": [ + {"key": "$current_url", "type": "event", "value": "?", "operator": "not_icontains"}, + {"key": "$referring_domain", "type": "event", "value": "google", "operator": "icontains"}, + ], + } + ], +} +properties_7 = { + "type": "AND", + "values": [ + {"type": "AND", "values": [{"type": "AND", "values": []}, {"type": "AND", "values": []}]}, + { + "type": "AND", + "values": [{"key": "dateDiff('minute', timestamp, now()) < 5", "type": "hogql", "value": None}], + }, + ], +} +properties_8 = { + "type": "AND", + "values": [ + { + "type": "AND", + "values": [{"key": "dateDiff('minute', timestamp, now()) < 5", "type": "hogql", "value": None}], + }, + { + "type": "AND", + "values": [{"key": "dateDiff('minute', timestamp, now()) < 5", "type": "hogql", "value": None}], + }, + ], +} +properties_9 = { + "type": "AND", + "values": [ + { + "type": "AND", + "values": [ + {"key": "$browser", "value": ["Chrome"], "operator": "exact", "type": "event"}, + {"key": "$browser", "value": ["Chrome"], "operator": "exact", "type": "person"}, + {"key": "$feature/hogql-insights", "value": ["true"], "operator": "exact", "type": "event"}, + { + "key": "site_url", + "value": ["http://localhost:8000"], + "operator": "exact", + "type": "group", + "group_type_index": 1, + }, + {"key": "id", "value": 2, "type": "cohort"}, + {"key": "tag_name", "value": ["elem"], "operator": "exact", "type": "element"}, + {"key": "$session_duration", "value": None, "operator": "gt", "type": "session"}, + {"type": "hogql", "key": "properties.name", "value": None}, + ], + }, + {"type": "OR", "values": [{}]}, + ], +} + +test_properties = [ + properties_0, + properties_1, + properties_2, + properties_3, + properties_4, + properties_5, + properties_6, + properties_7, + properties_8, + properties_9, +] + + +@pytest.mark.parametrize("properties", test_properties) +def test_base_properties(properties): + """smoke test (i.e. filter_to_query should not throw) for real world properties""" + filter = LegacyFilter(data={"properties": properties}) + filter_to_query(filter) + + +class TestFilterToQuery(BaseTest): + def test_base_trend(self): + filter = LegacyFilter(data={}) + + query = filter_to_query(filter) + + self.assertEqual(query.kind, "TrendsQuery") + + def test_full_trend(self): + filter = LegacyFilter(data={}) + + query = filter_to_query(filter) + + self.assertEqual( + query.model_dump(exclude_defaults=True), + { + "dateRange": {"date_from": "-7d"}, + "interval": "day", + "series": [], + "filterTestAccounts": False, + "breakdown": {"breakdown_normalize_url": False}, + "trendsFilter": { + "compare": False, + "display": ChartDisplayType.ActionsLineGraph, + "smoothing_intervals": 1, + }, + }, + ) + + def test_base_funnel(self): + filter = LegacyFilter(data={"insight": "FUNNELS"}) + + query = filter_to_query(filter) + + self.assertEqual(query.kind, "FunnelsQuery") + + def test_base_retention_query(self): + filter = LegacyFilter(data={"insight": "RETENTION"}) + + with pytest.raises(Exception) as exception: + filter_to_query(filter) + + self.assertEqual( + str(exception.value), + "Filter type does not match insight type RETENTION", + ) + + def test_base_retention_query_from_retention_filter(self): + filter = LegacyRetentionFilter(data={}) + + query = filter_to_query(filter) + + self.assertEqual(query.kind, "RetentionQuery") + + def test_base_paths_query(self): + filter = LegacyFilter(data={"insight": "PATHS"}) + + with pytest.raises(Exception) as exception: + filter_to_query(filter) + + self.assertEqual( + str(exception.value), + "Filter type does not match insight type PATHS", + ) + + def test_base_path_query_from_path_filter(self): + filter = LegacyPathFilter(data={}) + + query = filter_to_query(filter) + + self.assertEqual(query.kind, "PathsQuery") + + def test_base_lifecycle_query(self): + filter = LegacyFilter(data={"insight": "LIFECYCLE"}) + + query = filter_to_query(filter) + + self.assertEqual(query.kind, "LifecycleQuery") + + def test_base_stickiness_query(self): + filter = LegacyFilter(data={"insight": "STICKINESS"}) + + with pytest.raises(Exception) as exception: + filter_to_query(filter) + + self.assertEqual( + str(exception.value), + "Filter type does not match insight type STICKINESS", + ) + + def test_base_stickiness_query_from_stickiness_filter(self): + filter = LegacyStickinessFilter(data={}, team=self.team) + + query = filter_to_query(filter) + + self.assertEqual(query.kind, "StickinessQuery") + + def test_date_range_default(self): + filter = LegacyFilter(data={}) + + query = filter_to_query(filter) + + self.assertEqual(query.dateRange.date_from, "-7d") + self.assertEqual(query.dateRange.date_to, None) + + def test_date_range_custom(self): + filter = LegacyFilter(data={"date_from": "-14d", "date_to": "-7d"}) + + query = filter_to_query(filter) + + self.assertEqual(query.dateRange.date_from, "-14d") + self.assertEqual(query.dateRange.date_to, "-7d") + + def test_interval_default(self): + filter = LegacyFilter(data={}) + + query = filter_to_query(filter) + + self.assertEqual(query.interval, "day") + + def test_interval_custom(self): + filter = LegacyFilter(data={"interval": "hour"}) + + query = filter_to_query(filter) + + self.assertEqual(query.interval, "hour") + + def test_series_default(self): + filter = LegacyFilter(data={}) + + query = filter_to_query(filter) + + self.assertEqual(query.series, []) + + def test_series_custom(self): + filter = LegacyFilter( + data={ + "events": [{"id": "$pageview"}, {"id": "$pageview", "math": "dau"}], + "actions": [{"id": 1}, {"id": 1, "math": "dau"}], + } + ) + + query = filter_to_query(filter) + + self.assertEqual( + query.series, + [ + ActionsNode(id=1), + ActionsNode(id=1, math=BaseMathType.dau), + EventsNode(event="$pageview", name="$pageview"), + EventsNode(event="$pageview", name="$pageview", math=BaseMathType.dau), + ], + ) + + def test_series_order(self): + filter = LegacyFilter( + data={ + "events": [{"id": "$pageview", "order": 1}, {"id": "$pageview", "math": "dau", "order": 2}], + "actions": [{"id": 1, "order": 3}, {"id": 1, "math": "dau", "order": 0}], + } + ) + + query = filter_to_query(filter) + + self.assertEqual( + query.series, + [ + ActionsNode(id=1, math=BaseMathType.dau), + EventsNode(event="$pageview", name="$pageview"), + EventsNode(event="$pageview", name="$pageview", math=BaseMathType.dau), + ActionsNode(id=1), + ], + ) + + def test_series_math(self): + filter = LegacyFilter( + data={ + "events": [ + {"id": "$pageview", "math": "dau"}, # base math type + {"id": "$pageview", "math": "median", "math_property": "$math_prop"}, # property math type + {"id": "$pageview", "math": "avg_count_per_actor"}, # count per actor math type + {"id": "$pageview", "math": "unique_group", "math_group_type_index": 0}, # unique group + { + "id": "$pageview", + "math": "hogql", + "math_hogql": "avg(toInt(properties.$session_id)) + 1000", + }, # hogql + ] + } + ) + + query = filter_to_query(filter) + + self.assertEqual( + query.series, + [ + EventsNode(event="$pageview", name="$pageview", math=BaseMathType.dau), + EventsNode( + event="$pageview", name="$pageview", math=PropertyMathType.median, math_property="$math_prop" + ), + EventsNode(event="$pageview", name="$pageview", math=CountPerActorMathType.avg_count_per_actor), + EventsNode(event="$pageview", name="$pageview", math="unique_group", math_group_type_index=0), + EventsNode( + event="$pageview", + name="$pageview", + math="hogql", + math_hogql="avg(toInt(properties.$session_id)) + 1000", + ), + ], + ) + + def test_series_properties(self): + filter = LegacyFilter( + data={ + "events": [ + {"id": "$pageview", "properties": []}, # smoke test + { + "id": "$pageview", + "properties": [{"key": "success", "type": "event", "value": ["true"], "operator": "exact"}], + }, + { + "id": "$pageview", + "properties": [{"key": "email", "type": "person", "value": "is_set", "operator": "is_set"}], + }, + { + "id": "$pageview", + "properties": [{"key": "text", "value": ["some text"], "operator": "exact", "type": "element"}], + }, + { + "id": "$pageview", + "properties": [{"key": "$session_duration", "value": 1, "operator": "gt", "type": "session"}], + }, + {"id": "$pageview", "properties": [{"key": "id", "value": 2, "type": "cohort"}]}, + { + "id": "$pageview", + "properties": [ + { + "key": "name", + "value": ["Hedgebox Inc."], + "operator": "exact", + "type": "group", + "group_type_index": 2, + } + ], + }, + { + "id": "$pageview", + "properties": [ + {"key": "dateDiff('minute', timestamp, now()) < 30", "type": "hogql", "value": None} + ], + }, + { + "id": "$pageview", + "properties": [ + {"key": "$referring_domain", "type": "event", "value": "google", "operator": "icontains"}, + {"key": "utm_source", "type": "event", "value": "is_not_set", "operator": "is_not_set"}, + ], + }, + ] + } + ) + + query = filter_to_query(filter) + + self.assertEqual( + query.series, + [ + EventsNode(event="$pageview", name="$pageview", properties=[]), + EventsNode( + event="$pageview", + name="$pageview", + properties=[EventPropertyFilter(key="success", value=["true"], operator=PropertyOperator.exact)], + ), + EventsNode( + event="$pageview", + name="$pageview", + properties=[PersonPropertyFilter(key="email", value="is_set", operator=PropertyOperator.is_set)], + ), + EventsNode( + event="$pageview", + name="$pageview", + properties=[ + ElementPropertyFilter(key=Key.text, value=["some text"], operator=PropertyOperator.exact) + ], + ), + EventsNode( + event="$pageview", + name="$pageview", + properties=[SessionPropertyFilter(value=1, operator=PropertyOperator.gt)], + ), + EventsNode(event="$pageview", name="$pageview", properties=[CohortPropertyFilter(value=2)]), + EventsNode( + event="$pageview", + name="$pageview", + properties=[ + GroupPropertyFilter( + key="name", value=["Hedgebox Inc."], operator=PropertyOperator.exact, group_type_index=2 + ) + ], + ), + EventsNode( + event="$pageview", + name="$pageview", + properties=[HogQLPropertyFilter(key="dateDiff('minute', timestamp, now()) < 30")], + ), + EventsNode( + event="$pageview", + name="$pageview", + properties=[ + EventPropertyFilter( + key="$referring_domain", value="google", operator=PropertyOperator.icontains + ), + EventPropertyFilter(key="utm_source", value="is_not_set", operator=PropertyOperator.is_not_set), + ], + ), + ], + ) + + def test_breakdown(self): + filter = LegacyFilter(data={"breakdown_type": "event", "breakdown": "$browser"}) + + query = filter_to_query(filter) + + self.assertEqual( + query.breakdown, + BreakdownFilter(breakdown_type=BreakdownType.event, breakdown="$browser", breakdown_normalize_url=False), + ) + + def test_breakdown_converts_multi(self): + filter = LegacyFilter(data={"breakdowns": [{"type": "event", "property": "$browser"}]}) + + query = filter_to_query(filter) + + self.assertEqual( + query.breakdown, + BreakdownFilter(breakdown_type=BreakdownType.event, breakdown="$browser", breakdown_normalize_url=False), + ) + + def test_breakdown_type_default(self): + filter = LegacyFilter(data={"breakdown": "some_prop"}) + + query = filter_to_query(filter) + + self.assertEqual( + query.breakdown, + BreakdownFilter(breakdown_type=BreakdownType.event, breakdown="some_prop", breakdown_normalize_url=False), + ) + + def test_trends_filter(self): + filter = LegacyFilter( + data={ + "smoothing_intervals": 2, + "compare": True, + "aggregation_axis_format": "duration_ms", + "aggregation_axis_prefix": "pre", + "aggregation_axis_postfix": "post", + "formula": "A + B", + "shown_as": "Volume", + "display": "ActionsAreaGraph", + } + ) + + query = filter_to_query(filter) + + self.assertEqual( + query.trendsFilter, + TrendsFilter( + smoothing_intervals=2, + compare=True, + aggregation_axis_format=AggregationAxisFormat.duration_ms, + aggregation_axis_prefix="pre", + aggregation_axis_postfix="post", + formula="A + B", + shown_as=ShownAsValue.Volume, + display=ChartDisplayType.ActionsAreaGraph, + ), + ) + + def test_funnels_filter(self): + filter = LegacyFilter( + data={ + "insight": "FUNNELS", + "funnel_viz_type": "steps", + "funnel_window_interval_unit": "hour", + "funnel_window_interval": 13, + "breakdown_attribution_type": "step", + "breakdown_attribution_value": 2, + "funnel_order_type": "strict", + "funnel_aggregate_by_hogql": "person_id", + "exclusions": [ + { + "id": "$pageview", + "type": "events", + "order": 0, + "name": "$pageview", + "funnel_from_step": 1, + "funnel_to_step": 2, + } + ], + "bin_count": 15, # used in time to convert: number of bins to show in histogram + "funnel_from_step": 1, # used in time to convert: initial step index to compute time to convert + "funnel_to_step": 2, # used in time to convert: ending step index to compute time to convert + # + # frontend only params + # "layout": layout, + # "funnel_advanced":funnel_advanced, # unused, previously used to toggle advanced options on or off + # "funnel_step_reference": "previous", # whether conversion shown in graph should be across all steps or just from the previous step + # hidden_legend_keys # used to toggle visibilities in table and legend + # + # persons endpoint only params + # "funnel_step_breakdown": funnel_step_breakdown, # used in steps breakdown: persons modal + # "funnel_correlation_person_entity":funnel_correlation_person_entity, + # "funnel_correlation_person_converted":funnel_correlation_person_converted, # success or failure counts + # "entrance_period_start": entrance_period_start, # this and drop_off is used for funnels time conversion date for the persons modal + # "drop_off": drop_off, + # "funnel_step": funnel_step, + # "funnel_custom_steps": funnel_custom_steps, + } + ) + + query = filter_to_query(filter) + + self.assertEqual( + query.funnelsFilter, + FunnelsFilter( + funnel_viz_type=FunnelVizType.steps, + funnel_from_step=1, + funnel_to_step=2, + funnel_window_interval_unit=FunnelConversionWindowTimeUnit.hour, + funnel_window_interval=13, + breakdown_attribution_type=BreakdownAttributionType.step, + breakdown_attribution_value=2, + funnel_order_type=StepOrderValue.strict, + exclusions=[ + FunnelExclusion( + id="$pageview", + type=EntityType.events, + order=0, + name="$pageview", + funnel_from_step=1, + funnel_to_step=2, + ) + ], + bin_count=15, + funnel_aggregate_by_hogql="person_id", + funnel_custom_steps=[], + # funnel_step_reference=FunnelStepReference.previous, + ), + ) + + def test_retention_filter(self): + filter = LegacyRetentionFilter( + data={ + "retention_type": "retention_first_time", + # retention_reference="previous", + "total_intervals": 12, + "returning_entity": {"id": "$pageview", "name": "$pageview", "type": "events"}, + "target_entity": {"id": "$pageview", "name": "$pageview", "type": "events"}, + "period": "Week", + } + ) + + query = filter_to_query(filter) + + self.assertEqual( + query.retentionFilter, + RetentionFilter( + retention_type=RetentionType.retention_first_time, + total_intervals=12, + period=RetentionPeriod.Week, + returning_entity={ + "id": "$pageview", + "name": "$pageview", + "type": "events", + "custom_name": None, + "order": None, + }, + target_entity={ + "id": "$pageview", + "name": "$pageview", + "type": "events", + "custom_name": None, + "order": None, + }, + ), + ) + + def test_paths_filter(self): + filter = LegacyPathFilter( + data={ + "include_event_types": ["$pageview", "hogql"], + "start_point": "http://localhost:8000/events", + "end_point": "http://localhost:8000/home", + "paths_hogql_expression": "event", + "edge_limit": 50, + "min_edge_weight": 10, + "max_edge_weight": 20, + "local_path_cleaning_filters": [{"alias": "merchant", "regex": "\\/merchant\\/\\d+\\/dashboard$"}], + "path_replacements": True, + "exclude_events": ["http://localhost:8000/events"], + "step_limit": 5, + "path_groupings": ["/merchant/*/payment"], + "funnel_paths": "funnel_path_between_steps", + "funnel_filter": { + "insight": "FUNNELS", + "events": [ + {"type": "events", "id": "$pageview", "order": 0, "name": "$pageview", "math": "total"}, + {"type": "events", "id": None, "order": 1, "math": "total"}, + ], + "funnel_viz_type": "steps", + "exclusions": [], + "filter_test_accounts": True, + "funnel_step": 2, + }, + } + ) + + query = filter_to_query(filter) + + self.assertEqual( + query.pathsFilter, + PathsFilter( + include_event_types=[PathType.field_pageview, PathType.hogql], + paths_hogql_expression="event", + start_point="http://localhost:8000/events", + end_point="http://localhost:8000/home", + edge_limit=50, + min_edge_weight=10, + max_edge_weight=20, + local_path_cleaning_filters=[ + PathCleaningFilter(alias="merchant", regex="\\/merchant\\/\\d+\\/dashboard$") + ], + path_replacements=True, + exclude_events=["http://localhost:8000/events"], + step_limit=5, + path_groupings=["/merchant/*/payment"], + funnel_paths=FunnelPathType.funnel_path_between_steps, + funnel_filter={ + "insight": "FUNNELS", + "events": [ + {"type": "events", "id": "$pageview", "order": 0, "name": "$pageview", "math": "total"}, + {"type": "events", "id": None, "order": 1, "math": "total"}, + ], + "funnel_viz_type": "steps", + "exclusions": [], + "filter_test_accounts": True, + "funnel_step": 2, + }, + ), + ) + + def test_stickiness_filter(self): + filter = LegacyStickinessFilter( + data={"insight": "STICKINESS", "compare": True, "shown_as": "Stickiness"}, team=self.team + ) + + query = filter_to_query(filter) + + self.assertEqual( + query.stickinessFilter, + StickinessFilter(compare=True, shown_as=ShownAsValue.Stickiness), + ) + + def test_lifecycle_filter(self): + filter = LegacyFilter( + data={ + "insight": "LIFECYCLE", + "shown_as": "Lifecycle", + } + ) + + query = filter_to_query(filter) + + self.assertEqual( + query.lifecycleFilter, + LifecycleFilter( + shown_as=ShownAsValue.Lifecycle, + ), + ) diff --git a/posthog/models/filters/__init__.py b/posthog/models/filters/__init__.py index db19d8addc105..fa75a96fc7308 100644 --- a/posthog/models/filters/__init__.py +++ b/posthog/models/filters/__init__.py @@ -19,3 +19,5 @@ AnyFilter: TypeAlias = ( Filter | PathFilter | RetentionFilter | StickinessFilter | SessionRecordingsFilter | PropertiesTimelineFilter ) + +AnyInsightFilter: TypeAlias = Filter | PathFilter | RetentionFilter | StickinessFilter diff --git a/posthog/models/filters/filter.py b/posthog/models/filters/filter.py index e0549650981e6..816e1a846d7fe 100644 --- a/posthog/models/filters/filter.py +++ b/posthog/models/filters/filter.py @@ -1,5 +1,6 @@ from .base_filter import BaseFilter from .mixins.common import ( + AggregationAxisMixin, BreakdownMixin, BreakdownValueMixin, ClientQueryIdMixin, @@ -88,6 +89,7 @@ class Filter( UpdatedAfterMixin, ClientQueryIdMixin, SampleMixin, + AggregationAxisMixin, BaseFilter, ): """ diff --git a/posthog/models/filters/mixins/common.py b/posthog/models/filters/mixins/common.py index bbb727407c6be..b7303ea3e3ebf 100644 --- a/posthog/models/filters/mixins/common.py +++ b/posthog/models/filters/mixins/common.py @@ -592,3 +592,21 @@ def sampling_factor(self) -> Optional[float]: @include_dict def sampling_factor_to_dict(self): return {SAMPLING_FACTOR: self.sampling_factor or ""} + + +class AggregationAxisMixin(BaseParamMixin): + """ + Aggregation Axis. Only used frontend side. + """ + + @cached_property + def aggregation_axis_format(self) -> Optional[str]: + return self._data.get("aggregation_axis_format", None) + + @cached_property + def aggregation_axis_prefix(self) -> Optional[str]: + return self._data.get("aggregation_axis_prefix", None) + + @cached_property + def aggregation_axis_postfix(self) -> Optional[str]: + return self._data.get("aggregation_axis_postfix", None) diff --git a/posthog/models/filters/retention_filter.py b/posthog/models/filters/retention_filter.py index 0d9e1568c5d3d..cd767606a6dd1 100644 --- a/posthog/models/filters/retention_filter.py +++ b/posthog/models/filters/retention_filter.py @@ -45,7 +45,10 @@ class RetentionFilter( BaseFilter, ): def __init__(self, data: Dict[str, Any] = {}, request: Optional[Request] = None, **kwargs) -> None: - data["insight"] = INSIGHT_RETENTION + if data: + data["insight"] = INSIGHT_RETENTION + else: + data = {"insight": INSIGHT_RETENTION} super().__init__(data, request, **kwargs) @cached_property diff --git a/posthog/models/filters/stickiness_filter.py b/posthog/models/filters/stickiness_filter.py index 5327406c90b95..dbabdd5e6897a 100644 --- a/posthog/models/filters/stickiness_filter.py +++ b/posthog/models/filters/stickiness_filter.py @@ -4,6 +4,8 @@ from rest_framework.exceptions import ValidationError from rest_framework.request import Request +from posthog.constants import INSIGHT_STICKINESS + from .base_filter import BaseFilter from .mixins.common import ( ClientQueryIdMixin, @@ -54,6 +56,10 @@ class StickinessFilter( team: "Team" def __init__(self, data: Optional[Dict[str, Any]] = None, request: Optional[Request] = None, **kwargs) -> None: + if data: + data["insight"] = INSIGHT_STICKINESS + else: + data = {"insight": INSIGHT_STICKINESS} super().__init__(data, request, **kwargs) team: Optional["Team"] = kwargs.get("team", None) if not team: diff --git a/posthog/models/filters/test/test_stickiness_filter.py b/posthog/models/filters/test/test_stickiness_filter.py index b2b5d70bda46d..85ef23a2e83be 100644 --- a/posthog/models/filters/test/test_stickiness_filter.py +++ b/posthog/models/filters/test/test_stickiness_filter.py @@ -37,7 +37,7 @@ def test_filter_properties(self): "properties": {}, } ], - "insight": "TRENDS", + "insight": "STICKINESS", "interval": "month", "sampling_factor": 0.1, }, diff --git a/posthog/schema.py b/posthog/schema.py index b80f0163d3477..f54f5ee12e9c6 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -124,13 +124,10 @@ class ElementType(BaseModel): class EmptyPropertyFilter(BaseModel): + pass model_config = ConfigDict( extra="forbid", ) - key: Optional[Any] = None - operator: Optional[Any] = None - type: Optional[Any] = None - value: Optional[Any] = None class EntityType(str, Enum): @@ -185,18 +182,7 @@ class FunnelConversionWindowTimeUnit(str, Enum): month = "month" -class FunnelLayout(str, Enum): - horizontal = "horizontal" - vertical = "vertical" - - -class FunnelPathType(str, Enum): - funnel_path_before_step = "funnel_path_before_step" - funnel_path_between_steps = "funnel_path_between_steps" - funnel_path_after_step = "funnel_path_after_step" - - -class FunnelStepRangeEntityFilter(BaseModel): +class FunnelExclusion(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -210,6 +196,17 @@ class FunnelStepRangeEntityFilter(BaseModel): type: Optional[EntityType] = None +class FunnelLayout(str, Enum): + horizontal = "horizontal" + vertical = "vertical" + + +class FunnelPathType(str, Enum): + funnel_path_before_step = "funnel_path_before_step" + funnel_path_between_steps = "funnel_path_between_steps" + funnel_path_after_step = "funnel_path_after_step" + + class FunnelStepReference(str, Enum): total = "total" previous = "previous" @@ -554,7 +551,7 @@ class FunnelsFilter(BaseModel): breakdown_attribution_value: Optional[float] = None drop_off: Optional[bool] = None entrance_period_start: Optional[str] = None - exclusions: Optional[List[FunnelStepRangeEntityFilter]] = None + exclusions: Optional[List[FunnelExclusion]] = None funnel_advanced: Optional[bool] = None funnel_aggregate_by_hogql: Optional[str] = None funnel_correlation_person_converted: Optional[FunnelCorrelationPersonConverted] = None From 8597f7aa6c9e51c019672983779c8c5f68571be3 Mon Sep 17 00:00:00 2001 From: PostHog Bot <69588470+posthog-bot@users.noreply.github.com> Date: Tue, 26 Sep 2023 05:48:26 -0400 Subject: [PATCH 16/22] chore(deps): Update posthog-js to 1.81.0 (#17604) --- package.json | 2 +- pnpm-lock.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 0aaa5a82c9b1f..c0a5ae8b76aba 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "md5": "^2.3.0", "monaco-editor": "^0.39.0", "papaparse": "^5.4.1", - "posthog-js": "1.80.0", + "posthog-js": "1.81.0", "posthog-js-lite": "2.0.0-alpha5", "prettier": "^2.8.8", "prop-types": "^15.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78ba5bff58496..a85e2688bac8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -198,8 +198,8 @@ dependencies: specifier: ^5.4.1 version: 5.4.1 posthog-js: - specifier: 1.80.0 - version: 1.80.0 + specifier: 1.81.0 + version: 1.81.0 posthog-js-lite: specifier: 2.0.0-alpha5 version: 2.0.0-alpha5 @@ -15016,8 +15016,8 @@ packages: resolution: {integrity: sha512-tlkBdypJuvK/s00n4EiQjwYVfuuZv6vt8BF3g1ooIQa2Gz9Vz80p8q3qsPLZ0V5ErGRy6i3Q4fWC9TDzR7GNRQ==} dev: false - /posthog-js@1.80.0: - resolution: {integrity: sha512-GAbdSqNG1fsXqdmG2Wx9nuZbK/LlpDUGUC+OQyFWNRylGAczSc8TIEErupQYxDmybxtC7f2/1Jtw/fgyVNLnRA==} + /posthog-js@1.81.0: + resolution: {integrity: sha512-Ax+qzJQtJjViZ9jJPS3sf2vlDd/xZHVHcEik3tEOY0LXLgJZhbUymqW1Lo3sqsKRvKjHSidPsodTpm5/K63h+Q==} dependencies: fflate: 0.4.8 dev: false From 0b9e7ea416ff91dc659ffa84fba2667f0af46ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Oberm=C3=BCller?= Date: Tue, 26 Sep 2023 12:44:27 +0200 Subject: [PATCH 17/22] refactor(hogql): remove persons related params from insight filters (#17612) --- .../utils/filtersToQueryNode.test.ts | 14 ----- .../InsightQuery/utils/filtersToQueryNode.ts | 12 ---- frontend/src/queries/schema.json | 62 +------------------ frontend/src/queries/schema.ts | 27 +++++--- .../src/scenes/insights/utils/cleanFilters.ts | 1 - frontend/src/types.ts | 53 ++++++++++------ .../legacy_compatibility/filter_to_query.py | 8 --- .../test/test_filter_to_query.py | 2 - posthog/schema.py | 17 ----- 9 files changed, 55 insertions(+), 141 deletions(-) diff --git a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts index ab120558a72c5..523179c5e4071 100644 --- a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts +++ b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts @@ -354,7 +354,6 @@ describe('filtersToQueryNode', () => { funnel_correlation_person_entity: { a: 1 }, funnel_correlation_person_converted: 'true', funnel_custom_steps: [1, 2, 3], - funnel_advanced: true, layout: FunnelLayout.horizontal, funnel_step: 1, entrance_period_start: 'abc', @@ -371,7 +370,6 @@ describe('filtersToQueryNode', () => { funnel_from_step: 1, funnel_to_step: 2, funnel_step_reference: FunnelStepReference.total, - funnel_step_breakdown: 1, breakdown_attribution_type: BreakdownAttributionType.AllSteps, breakdown_attribution_value: 1, bin_count: 'auto', @@ -384,14 +382,7 @@ describe('filtersToQueryNode', () => { funnel_to_step: 1, }, ], - funnel_correlation_person_entity: { a: 1 }, - funnel_correlation_person_converted: 'true', - funnel_custom_steps: [1, 2, 3], - funnel_advanced: true, layout: FunnelLayout.horizontal, - funnel_step: 1, - entrance_period_start: 'abc', - drop_off: true, hidden_legend_breakdowns: ['Chrome', 'Safari'], }, } @@ -465,9 +456,6 @@ describe('filtersToQueryNode', () => { funnel_filter: { a: 1 }, exclude_events: ['e', 'f'], step_limit: 1, - path_start_key: 'g', - path_end_key: 'h', - path_dropoff_key: 'i', path_replacements: true, local_path_cleaning_filters: [{ alias: 'home' }], edge_limit: 1, @@ -486,7 +474,6 @@ describe('filtersToQueryNode', () => { compare: true, show_legend: true, hidden_legend_keys: { 0: true, 10: true }, - stickiness_days: 2, shown_as: ShownAsValue.STICKINESS, display: ChartDisplayType.ActionsLineGraph, } @@ -499,7 +486,6 @@ describe('filtersToQueryNode', () => { compare: true, show_legend: true, hidden_legend_indexes: [0, 10], - stickiness_days: 2, shown_as: ShownAsValue.STICKINESS, display: ChartDisplayType.ActionsLineGraph, }, diff --git a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts index 83aea93627782..5137fbf5b2116 100644 --- a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts +++ b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts @@ -191,7 +191,6 @@ export const filtersToQueryNode = (filters: Partial): InsightQueryNo funnel_from_step: filters.funnel_from_step, funnel_to_step: filters.funnel_to_step, funnel_step_reference: filters.funnel_step_reference, - funnel_step_breakdown: filters.funnel_step_breakdown, breakdown_attribution_type: filters.breakdown_attribution_type, breakdown_attribution_value: filters.breakdown_attribution_value, bin_count: filters.bin_count, @@ -199,14 +198,7 @@ export const filtersToQueryNode = (filters: Partial): InsightQueryNo funnel_window_interval: filters.funnel_window_interval, funnel_order_type: filters.funnel_order_type, exclusions: filters.exclusions, - funnel_correlation_person_entity: filters.funnel_correlation_person_entity, - funnel_correlation_person_converted: filters.funnel_correlation_person_converted, - funnel_custom_steps: filters.funnel_custom_steps, - funnel_advanced: filters.funnel_advanced, layout: filters.layout, - funnel_step: filters.funnel_step, - entrance_period_start: filters.entrance_period_start, - drop_off: filters.drop_off, hidden_legend_breakdowns: cleanHiddenLegendSeries(filters.hidden_legend_keys), funnel_aggregate_by_hogql: filters.funnel_aggregate_by_hogql, }) @@ -238,9 +230,6 @@ export const filtersToQueryNode = (filters: Partial): InsightQueryNo funnel_filter: filters.funnel_filter, exclude_events: filters.exclude_events, step_limit: filters.step_limit, - path_start_key: filters.path_start_key, - path_end_key: filters.path_end_key, - path_dropoff_key: filters.path_dropoff_key, path_replacements: filters.path_replacements, local_path_cleaning_filters: filters.local_path_cleaning_filters, edge_limit: filters.edge_limit, @@ -256,7 +245,6 @@ export const filtersToQueryNode = (filters: Partial): InsightQueryNo compare: filters.compare, show_legend: filters.show_legend, hidden_legend_indexes: cleanHiddenLegendIndexes(filters.hidden_legend_keys), - stickiness_days: filters.stickiness_days, shown_as: filters.shown_as, show_values_on_series: filters.show_values_on_series, }) diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 80eefe5462b0a..7edaeb0039cf1 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -894,7 +894,7 @@ }, "FunnelsFilter": { "additionalProperties": false, - "description": "`FunnelsFilterType` minus everything inherited from `FilterType` and `hidden_legend_keys` replaced by `hidden_legend_breakdowns`", + "description": "`FunnelsFilterType` minus everything inherited from `FilterType` and persons modal related params and `hidden_legend_keys` replaced by `hidden_legend_breakdowns`", "properties": { "bin_count": { "$ref": "#/definitions/BinCountValue" @@ -905,65 +905,21 @@ "breakdown_attribution_value": { "type": "number" }, - "drop_off": { - "type": "boolean" - }, - "entrance_period_start": { - "type": "string" - }, "exclusions": { "items": { "$ref": "#/definitions/FunnelExclusion" }, "type": "array" }, - "funnel_advanced": { - "type": "boolean" - }, "funnel_aggregate_by_hogql": { "type": "string" }, - "funnel_correlation_person_converted": { - "enum": ["true", "false"], - "type": "string" - }, - "funnel_correlation_person_entity": { - "type": "object" - }, - "funnel_custom_steps": { - "items": { - "type": "number" - }, - "type": "array" - }, "funnel_from_step": { "type": "number" }, "funnel_order_type": { "$ref": "#/definitions/StepOrderValue" }, - "funnel_step": { - "type": "number" - }, - "funnel_step_breakdown": { - "anyOf": [ - { - "type": "string" - }, - { - "items": { - "type": "number" - }, - "type": "array" - }, - { - "type": "number" - }, - { - "type": "null" - } - ] - }, "funnel_step_reference": { "$ref": "#/definitions/FunnelStepReference" }, @@ -1472,7 +1428,7 @@ }, "PathsFilter": { "additionalProperties": false, - "description": "`PathsFilterType` minus everything inherited from `FilterType`", + "description": "`PathsFilterType` minus everything inherited from `FilterType` and persons modal related params", "properties": { "edge_limit": { "type": "number" @@ -1510,12 +1466,6 @@ "min_edge_weight": { "type": "number" }, - "path_dropoff_key": { - "type": "string" - }, - "path_end_key": { - "type": "string" - }, "path_groupings": { "items": { "type": "string" @@ -1525,9 +1475,6 @@ "path_replacements": { "type": "boolean" }, - "path_start_key": { - "type": "string" - }, "path_type": { "$ref": "#/definitions/PathType" }, @@ -2051,7 +1998,7 @@ }, "StickinessFilter": { "additionalProperties": false, - "description": "`StickinessFilterType` minus everything inherited from `FilterType` and `hidden_legend_keys` replaced by `hidden_legend_indexes`", + "description": "`StickinessFilterType` minus everything inherited from `FilterType` and persons modal related params and `hidden_legend_keys` replaced by `hidden_legend_indexes`", "properties": { "compare": { "type": "boolean" @@ -2073,9 +2020,6 @@ }, "shown_as": { "$ref": "#/definitions/ShownAsValue" - }, - "stickiness_days": { - "type": "number" } }, "type": "object" diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 7461e85ec75fa..54c296a3521a0 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -393,11 +393,19 @@ export interface TrendsQuery extends InsightsQueryBase { response?: TrendsQueryResponse } -/** `FunnelsFilterType` minus everything inherited from `FilterType` and - * `hidden_legend_keys` replaced by `hidden_legend_breakdowns` */ +/** `FunnelsFilterType` minus everything inherited from `FilterType` and persons modal related params + * and `hidden_legend_keys` replaced by `hidden_legend_breakdowns` */ export type FunnelsFilter = Omit< FunnelsFilterType & { hidden_legend_breakdowns?: string[] }, - keyof FilterType | 'hidden_legend_keys' + | keyof FilterType + | 'hidden_legend_keys' + | 'funnel_step_breakdown' + | 'funnel_correlation_person_entity' + | 'funnel_correlation_person_converted' + | 'entrance_period_start' + | 'drop_off' + | 'funnel_step' + | 'funnel_custom_steps' > export interface FunnelsQuery extends InsightsQueryBase { kind: NodeKind.FunnelsQuery @@ -419,19 +427,22 @@ export interface RetentionQuery extends InsightsQueryBase { retentionFilter?: RetentionFilter } -/** `PathsFilterType` minus everything inherited from `FilterType` */ -export type PathsFilter = Omit +/** `PathsFilterType` minus everything inherited from `FilterType` and persons modal related params */ +export type PathsFilter = Omit< + PathsFilterType, + keyof FilterType | 'path_start_key' | 'path_end_key' | 'path_dropoff_key' +> export interface PathsQuery extends InsightsQueryBase { kind: NodeKind.PathsQuery /** Properties specific to the paths insight */ pathsFilter?: PathsFilter } -/** `StickinessFilterType` minus everything inherited from `FilterType` and - * `hidden_legend_keys` replaced by `hidden_legend_indexes` */ +/** `StickinessFilterType` minus everything inherited from `FilterType` and persons modal related params + * and `hidden_legend_keys` replaced by `hidden_legend_indexes` */ export type StickinessFilter = Omit< StickinessFilterType & { hidden_legend_indexes?: number[] }, - keyof FilterType | 'hidden_legend_keys' + keyof FilterType | 'hidden_legend_keys' | 'stickiness_days' > export interface StickinessQuery extends InsightsQueryBase { kind: NodeKind.StickinessQuery diff --git a/frontend/src/scenes/insights/utils/cleanFilters.ts b/frontend/src/scenes/insights/utils/cleanFilters.ts index 918b31758b712..bd16e3ca1ba79 100644 --- a/frontend/src/scenes/insights/utils/cleanFilters.ts +++ b/frontend/src/scenes/insights/utils/cleanFilters.ts @@ -232,7 +232,6 @@ export function cleanFilters( ...(filters.funnel_window_interval ? { funnel_window_interval: filters.funnel_window_interval } : {}), ...(filters.funnel_order_type ? { funnel_order_type: filters.funnel_order_type } : {}), ...(filters.hidden_legend_keys ? { hidden_legend_keys: filters.hidden_legend_keys } : {}), - ...(filters.funnel_advanced ? { funnel_advanced: filters.funnel_advanced } : {}), ...(filters.funnel_aggregate_by_hogql ? { funnel_aggregate_by_hogql: filters.funnel_aggregate_by_hogql } : {}), diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 275561203e954..9c0d54b3d6733 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1668,36 +1668,40 @@ export interface TrendsFilterType extends FilterType { // number of intervals, e.g. for a day interval, we may want to smooth over // 7 days to remove weekly variation. Smoothing is performed as a moving average. smoothing_intervals?: number + compare?: boolean + formula?: string + shown_as?: ShownAsValue + display?: ChartDisplayType + breakdown_histogram_bin_count?: number // trends breakdown histogram bin count + + // frontend only show_legend?: boolean // used to show/hide legend next to insights graph hidden_legend_keys?: Record // used to toggle visibilities in table and legend - compare?: boolean aggregation_axis_format?: AggregationAxisFormat // a fixed format like duration that needs calculation aggregation_axis_prefix?: string // a prefix to add to the aggregation axis e.g. £ aggregation_axis_postfix?: string // a postfix to add to the aggregation axis e.g. % - formula?: string - shown_as?: ShownAsValue - display?: ChartDisplayType show_values_on_series?: boolean show_percent_stack_view?: boolean - breakdown_histogram_bin_count?: number // trends breakdown histogram bin count } export interface StickinessFilterType extends FilterType { compare?: boolean - show_legend?: boolean // used to show/hide legend next to insights graph - hidden_legend_keys?: Record // used to toggle visibilities in table and legend - stickiness_days?: number shown_as?: ShownAsValue display?: ChartDisplayType + + // frontend only + show_legend?: boolean // used to show/hide legend next to insights graph + hidden_legend_keys?: Record // used to toggle visibilities in table and legend show_values_on_series?: boolean + + // persons only + stickiness_days?: number } export interface FunnelsFilterType extends FilterType { funnel_viz_type?: FunnelVizType // parameter sent to funnels API for time conversion code path funnel_from_step?: number // used in time to convert: initial step index to compute time to convert funnel_to_step?: number // used in time to convert: ending step index to compute time to convert - funnel_step_reference?: FunnelStepReference // whether conversion shown in graph should be across all steps or just from the previous step - funnel_step_breakdown?: string | number[] | number | null // used in steps breakdown: persons modal breakdown_attribution_type?: BreakdownAttributionType // funnels breakdown attribution type breakdown_attribution_value?: number // funnels breakdown attribution specific step value bin_count?: BinCountValue // used in time to convert: number of bins to show in histogram @@ -1705,16 +1709,21 @@ export interface FunnelsFilterType extends FilterType { funnel_window_interval?: number | undefined // length of conversion window funnel_order_type?: StepOrderValue exclusions?: FunnelExclusion[] // used in funnel exclusion filters - funnel_correlation_person_entity?: Record // Funnel Correlation Persons Filter - funnel_correlation_person_converted?: 'true' | 'false' // Funnel Correlation Persons Converted - success or failure counts - funnel_custom_steps?: number[] // used to provide custom steps for which to get people in a funnel - primarily for correlation use - funnel_advanced?: boolean // used to toggle advanced options on or off + funnel_aggregate_by_hogql?: string + + // frontend only layout?: FunnelLayout // used only for funnels - funnel_step?: number + funnel_step_reference?: FunnelStepReference // whether conversion shown in graph should be across all steps or just from the previous step + hidden_legend_keys?: Record // used to toggle visibilities in table and legend + + // persons only entrance_period_start?: string // this and drop_off is used for funnels time conversion date for the persons modal drop_off?: boolean - hidden_legend_keys?: Record // used to toggle visibilities in table and legend - funnel_aggregate_by_hogql?: string + funnel_step?: number + funnel_step_breakdown?: string | number[] | number | null // used in steps breakdown: persons modal + funnel_custom_steps?: number[] // used to provide custom steps for which to get people in a funnel - primarily for correlation use + funnel_correlation_person_entity?: Record // Funnel Correlation Persons Filter + funnel_correlation_person_converted?: 'true' | 'false' // Funnel Correlation Persons Converted - success or failure counts } export interface PathsFilterType extends FilterType { path_type?: PathType @@ -1727,14 +1736,16 @@ export interface PathsFilterType extends FilterType { funnel_filter?: Record // Funnel Filter used in Paths exclude_events?: string[] // Paths Exclusion type step_limit?: number // Paths Step Limit - path_start_key?: string // Paths People Start Key - path_end_key?: string // Paths People End Key - path_dropoff_key?: string // Paths People Dropoff Key path_replacements?: boolean local_path_cleaning_filters?: PathCleaningFilter[] edge_limit?: number | undefined // Paths edge limit min_edge_weight?: number | undefined // Paths max_edge_weight?: number | undefined // Paths + + // persons only + path_start_key?: string // Paths People Start Key + path_end_key?: string // Paths People End Key + path_dropoff_key?: string // Paths People Dropoff Key } export interface RetentionFilterType extends FilterType { retention_type?: RetentionType @@ -1746,6 +1757,8 @@ export interface RetentionFilterType extends FilterType { } export interface LifecycleFilterType extends FilterType { shown_as?: ShownAsValue + + // frontend only show_values_on_series?: boolean toggledLifecycles?: LifecycleToggle[] } diff --git a/posthog/hogql_queries/legacy_compatibility/filter_to_query.py b/posthog/hogql_queries/legacy_compatibility/filter_to_query.py index 3602dbcc538cd..91e1cdaa75b4b 100644 --- a/posthog/hogql_queries/legacy_compatibility/filter_to_query.py +++ b/posthog/hogql_queries/legacy_compatibility/filter_to_query.py @@ -158,7 +158,6 @@ def _insight_filter(filter: AnyInsightFilter): funnel_window_interval_unit=filter.funnel_window_interval_unit, funnel_window_interval=filter.funnel_window_interval, # funnel_step_reference=filter.funnel_step_reference, - # funnel_step_breakdown=filter.funnel_step_breakdown, breakdown_attribution_type=filter.breakdown_attribution_type, breakdown_attribution_value=filter.breakdown_attribution_value, bin_count=filter.bin_count, @@ -170,16 +169,9 @@ def _insight_filter(filter: AnyInsightFilter): ) for entity in filter.exclusions ], - funnel_custom_steps=filter.funnel_custom_steps, - # funnel_advanced=filter.funnel_advanced, layout=filter.layout, - funnel_step=filter.funnel_step, - entrance_period_start=filter.entrance_period_start, - drop_off=filter.drop_off, # hidden_legend_breakdowns: cleanHiddenLegendSeries(filters.hidden_legend_keys), funnel_aggregate_by_hogql=filter.funnel_aggregate_by_hogql, - # funnel_correlation_person_entity=filter.funnel_correlation_person_entity, - # funnel_correlation_person_converted=filter.funnel_correlation_person_converted, ), } elif filter.insight == "RETENTION" and isinstance(filter, LegacyRetentionFilter): diff --git a/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py b/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py index 6f1fe48d02c8a..6359b9e3e808d 100644 --- a/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py +++ b/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py @@ -832,7 +832,6 @@ def test_funnels_filter(self): # # frontend only params # "layout": layout, - # "funnel_advanced":funnel_advanced, # unused, previously used to toggle advanced options on or off # "funnel_step_reference": "previous", # whether conversion shown in graph should be across all steps or just from the previous step # hidden_legend_keys # used to toggle visibilities in table and legend # @@ -872,7 +871,6 @@ def test_funnels_filter(self): ], bin_count=15, funnel_aggregate_by_hogql="person_id", - funnel_custom_steps=[], # funnel_step_reference=FunnelStepReference.previous, ), ) diff --git a/posthog/schema.py b/posthog/schema.py index f54f5ee12e9c6..207fb07d6e62c 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -218,11 +218,6 @@ class FunnelVizType(str, Enum): trends = "trends" -class FunnelCorrelationPersonConverted(str, Enum): - true = "true" - false = "false" - - class HogQLNotice(BaseModel): model_config = ConfigDict( extra="forbid", @@ -275,11 +270,8 @@ class PathsFilter(BaseModel): local_path_cleaning_filters: Optional[List[PathCleaningFilter]] = None max_edge_weight: Optional[float] = None min_edge_weight: Optional[float] = None - path_dropoff_key: Optional[str] = None - path_end_key: Optional[str] = None path_groupings: Optional[List[str]] = None path_replacements: Optional[bool] = None - path_start_key: Optional[str] = None path_type: Optional[PathType] = None paths_hogql_expression: Optional[str] = None start_point: Optional[str] = None @@ -437,7 +429,6 @@ class StickinessFilter(BaseModel): show_legend: Optional[bool] = None show_values_on_series: Optional[bool] = None shown_as: Optional[ShownAsValue] = None - stickiness_days: Optional[float] = None class TimeToSeeDataSessionsQueryResponse(BaseModel): @@ -549,18 +540,10 @@ class FunnelsFilter(BaseModel): bin_count: Optional[Union[float, str]] = None breakdown_attribution_type: Optional[BreakdownAttributionType] = None breakdown_attribution_value: Optional[float] = None - drop_off: Optional[bool] = None - entrance_period_start: Optional[str] = None exclusions: Optional[List[FunnelExclusion]] = None - funnel_advanced: Optional[bool] = None funnel_aggregate_by_hogql: Optional[str] = None - funnel_correlation_person_converted: Optional[FunnelCorrelationPersonConverted] = None - funnel_correlation_person_entity: Optional[Dict[str, Any]] = None - funnel_custom_steps: Optional[List[float]] = None funnel_from_step: Optional[float] = None funnel_order_type: Optional[StepOrderValue] = None - funnel_step: Optional[float] = None - funnel_step_breakdown: Optional[Union[str, List[float], float]] = None funnel_step_reference: Optional[FunnelStepReference] = None funnel_to_step: Optional[float] = None funnel_viz_type: Optional[FunnelVizType] = None From ec4faf2d75648c4bac7d97dd6fb92565b48ff6e0 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Tue, 26 Sep 2023 13:10:26 +0100 Subject: [PATCH 18/22] fix: selecting from cohort people (#17610) * fix: selecting from cohort people * add a test * include numbers table in python test * fix FE example for numbers * always include the person and cohort id * Update query snapshots * Update query snapshots --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .../data-management/database/DatabaseTables.tsx | 4 +++- posthog/hogql/database/schema/cohort_people.py | 6 ++---- posthog/hogql/database/test/test_database.py | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/frontend/src/scenes/data-management/database/DatabaseTables.tsx b/frontend/src/scenes/data-management/database/DatabaseTables.tsx index d1f9b1ec50b01..1952603fd5845 100644 --- a/frontend/src/scenes/data-management/database/DatabaseTables.tsx +++ b/frontend/src/scenes/data-management/database/DatabaseTables.tsx @@ -88,7 +88,9 @@ export function DatabaseTables({ // TODO: Use `hogql` tag? query: `SELECT ${obj.columns .filter(({ table, fields, chain }) => !table && !fields && !chain) - .map(({ key }) => key)} FROM ${table} LIMIT 100`, + .map(({ key }) => key)} FROM ${ + table === 'numbers' ? 'numbers(0, 10)' : table + } LIMIT 100`, }, } return ( diff --git a/posthog/hogql/database/schema/cohort_people.py b/posthog/hogql/database/schema/cohort_people.py index 023690f4f97d9..ee5202fe9ed2a 100644 --- a/posthog/hogql/database/schema/cohort_people.py +++ b/posthog/hogql/database/schema/cohort_people.py @@ -23,11 +23,9 @@ def select_from_cohort_people_table(requested_fields: Dict[str, List[str]]): table_name = "raw_cohort_people" + # must always include the person and cohort ids regardless of what other fields are requested requested_fields = {"person_id": ["person_id"], "cohort_id": ["cohort_id"], **requested_fields} - - fields: List[ast.Expr] = [ - ast.Alias(alias=name, expr=ast.Field(chain=[table_name] + chain)) for name, chain in requested_fields.items() - ] + fields: List[ast.Expr] = [ast.Field(chain=[table_name] + chain) for name, chain in requested_fields.items()] return ast.SelectQuery( select=fields, diff --git a/posthog/hogql/database/test/test_database.py b/posthog/hogql/database/test/test_database.py index c90778f6cf308..0b5f6b6cafd46 100644 --- a/posthog/hogql/database/test/test_database.py +++ b/posthog/hogql/database/test/test_database.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest from django.test import override_settings +from parameterized import parameterized from posthog.hogql.database.database import create_hogql_database, serialize_database from posthog.test.base import BaseTest @@ -26,6 +27,21 @@ def test_serialize_database_with_person_on_events_enabled(self): serialized_database = serialize_database(create_hogql_database(team_id=self.team.pk)) assert json.dumps(serialized_database, indent=4) == self.snapshot + @parameterized.expand([False, True]) + def test_can_select_from_each_table_at_all(self, poe_enabled: bool) -> None: + with override_settings(PERSON_ON_EVENTS_OVERRIDE=poe_enabled): + serialized_database = serialize_database(create_hogql_database(team_id=self.team.pk)) + for table, possible_columns in serialized_database.items(): + if table == "numbers": + execute_hogql_query("SELECT number FROM numbers(10) LIMIT 100", self.team) + else: + columns = [ + x["key"] + for x in possible_columns + if "table" not in x and "chain" not in x and "fields" not in x + ] + execute_hogql_query(f"SELECT {','.join(columns)} FROM {table}", team=self.team) + @patch("posthog.hogql.query.sync_execute", return_value=(None, None)) @pytest.mark.usefixtures("unittest_snapshot") def test_database_with_warehouse_tables(self, patch_execute): From c3e99858d57d08f3d15b03ae92d45ca698802ac4 Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Tue, 26 Sep 2023 13:35:54 +0100 Subject: [PATCH 19/22] feat(surveys): Add UI for enabling/disabling surveys popup (#17602) --- bin/copy-posthog-js | 1 + frontend/src/lib/constants.tsx | 1 + .../src/scenes/project/Settings/Survey.tsx | 20 ++++++ .../src/scenes/project/Settings/index.tsx | 5 ++ .../src/scenes/surveys/SurveySettings.tsx | 64 +++++++++++++++++++ frontend/src/scenes/surveys/Surveys.tsx | 30 +++++++++ frontend/src/scenes/surveys/surveyLogic.tsx | 24 +++++-- frontend/src/types.ts | 1 + posthog/api/decide.py | 2 + posthog/api/team.py | 2 + 10 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 frontend/src/scenes/project/Settings/Survey.tsx create mode 100644 frontend/src/scenes/surveys/SurveySettings.tsx diff --git a/bin/copy-posthog-js b/bin/copy-posthog-js index 72fb6c6ec1fd6..ed66c58018772 100755 --- a/bin/copy-posthog-js +++ b/bin/copy-posthog-js @@ -8,3 +8,4 @@ cp node_modules/posthog-js/dist/array.js* frontend/dist/ cp node_modules/posthog-js/dist/array.full.js* frontend/dist/ cp node_modules/posthog-js/dist/recorder.js* frontend/dist/ cp node_modules/posthog-js/dist/recorder-v2.js* frontend/dist/ +cp node_modules/posthog-js/dist/surveys.js* frontend/dist/ diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index 1842e4f2adb4f..099091755bc87 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -166,6 +166,7 @@ export const FEATURE_FLAGS = { SESSION_REPLAY_CORS_PROXY: 'session-replay-cors-proxy', // owner: #team-monitoring HOGQL_INSIGHTS: 'hogql-insights', // owner: @mariusandra WEBHOOKS_DENYLIST: 'webhooks-denylist', // owner: #team-pipeline + SURVEYS_SITE_APP_DEPRECATION: 'surveys-site-app-deprecation', // owner: @neilkakkar } as const export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS] diff --git a/frontend/src/scenes/project/Settings/Survey.tsx b/frontend/src/scenes/project/Settings/Survey.tsx new file mode 100644 index 0000000000000..a33f4cdd9cb13 --- /dev/null +++ b/frontend/src/scenes/project/Settings/Survey.tsx @@ -0,0 +1,20 @@ +import { LemonDivider, Link } from '@posthog/lemon-ui' +import { SurveySettings as BasicSurveySettings } from 'scenes/surveys/SurveySettings' +import { urls } from 'scenes/urls' + +export function SurveySettings(): JSX.Element { + return ( + <> +

+ Surveys +

+

+ Get qualitative and quantitative data on how your users are doing. Surveys are found in the{' '} + surveys page. +

+ + + + + ) +} diff --git a/frontend/src/scenes/project/Settings/index.tsx b/frontend/src/scenes/project/Settings/index.tsx index f56c12247aff7..03622977b85f6 100644 --- a/frontend/src/scenes/project/Settings/index.tsx +++ b/frontend/src/scenes/project/Settings/index.tsx @@ -36,6 +36,9 @@ import { IngestionInfo } from './IngestionInfo' import { ExtraTeamSettings } from './ExtraTeamSettings' import { WeekStartConfig } from './WeekStartConfig' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { SurveySettings } from './Survey' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' export const scene: SceneExport = { component: ProjectSettings, @@ -75,6 +78,7 @@ export function ProjectSettings(): JSX.Element { const { location } = useValues(router) const { user, hasAvailableFeature } = useValues(userLogic) const hasAdvancedPaths = user?.organization?.available_features?.includes(AvailableFeature.PATHS_ADVANCED) + const { featureFlags } = useValues(featureFlagLogic) useAnchor(location.hash) @@ -245,6 +249,7 @@ export function ProjectSettings(): JSX.Element { + {featureFlags[FEATURE_FLAGS.SURVEYS_SITE_APP_DEPRECATION] && } diff --git a/frontend/src/scenes/surveys/SurveySettings.tsx b/frontend/src/scenes/surveys/SurveySettings.tsx new file mode 100644 index 0000000000000..d371980f708af --- /dev/null +++ b/frontend/src/scenes/surveys/SurveySettings.tsx @@ -0,0 +1,64 @@ +import { useActions, useValues } from 'kea' +import { teamLogic } from 'scenes/teamLogic' +import { LemonSwitch, Link } from '@posthog/lemon-ui' +import { urls } from 'scenes/urls' +import { LemonDialog } from 'lib/lemon-ui/LemonDialog' + +export type SurveySettingsProps = { + inModal?: boolean +} + +export function SurveySettings({ inModal = false }: SurveySettingsProps): JSX.Element { + const { updateCurrentTeam } = useActions(teamLogic) + const { currentTeam } = useValues(teamLogic) + + return ( +
+
+ { + updateCurrentTeam({ + surveys_opt_in: checked, + }) + }} + label="Enable surveys popup" + bordered={!inModal} + fullWidth={inModal} + labelClassName={inModal ? 'text-base font-semibold' : ''} + checked={!!currentTeam?.surveys_opt_in} + /> + +

+ Please note your website needs to have the{' '} + PostHog snippet or the latest version of{' '} + + posthog-js + {' '} + directly installed. For more details, check out our{' '} + + docs + + . +

+
+
+ ) +} + +export function openSurveysSettingsDialog(): void { + LemonDialog.open({ + title: 'Surveys settings', + content: , + width: 600, + primaryButton: { + children: 'Done', + }, + }) +} diff --git a/frontend/src/scenes/surveys/Surveys.tsx b/frontend/src/scenes/surveys/Surveys.tsx index 3f18b2df4e154..46032cf2d0709 100644 --- a/frontend/src/scenes/surveys/Surveys.tsx +++ b/frontend/src/scenes/surveys/Surveys.tsx @@ -16,6 +16,13 @@ import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductI import { userLogic } from 'scenes/userLogic' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { dayjs } from 'lib/dayjs' +import { VersionCheckerBanner } from 'lib/components/VersionChecker/VersionCheckerBanner' +import { teamLogic } from 'scenes/teamLogic' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { IconSettings } from 'lib/lemon-ui/icons' +import { openSurveysSettingsDialog } from './SurveySettings' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' export const scene: SceneExport = { component: Surveys, @@ -32,6 +39,10 @@ export function Surveys(): JSX.Element { const { nonArchivedSurveys, archivedSurveys, surveys, surveysLoading } = useValues(surveysLogic) const { deleteSurvey, updateSurvey } = useActions(surveysLogic) const { user } = useValues(userLogic) + const { featureFlags } = useValues(featureFlagLogic) + + const { currentTeam } = useValues(teamLogic) + const surveysPopupDisabled = currentTeam && !currentTeam?.surveys_opt_in const [tab, setSurveyTab] = useState(SurveysTabs.All) const shouldShowEmptyState = !surveysLoading && surveys.length === 0 @@ -61,6 +72,25 @@ export function Surveys(): JSX.Element { { key: SurveysTabs.Archived, label: 'Archived surveys' }, ]} /> + {featureFlags[FEATURE_FLAGS.SURVEYS_SITE_APP_DEPRECATION] && ( +
+ + + {surveysPopupDisabled ? ( + , + onClick: () => openSurveysSettingsDialog(), + children: 'Configure', + }} + > + Survey popups are currently disabled for this project. + + ) : null} +
+ )} {surveysLoading ? ( ) : ( diff --git a/frontend/src/scenes/surveys/surveyLogic.tsx b/frontend/src/scenes/surveys/surveyLogic.tsx index cdec952069295..3b8b55107870a 100644 --- a/frontend/src/scenes/surveys/surveyLogic.tsx +++ b/frontend/src/scenes/surveys/surveyLogic.tsx @@ -24,6 +24,8 @@ import { dayjs } from 'lib/dayjs' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' +import { featureFlagLogic as enabledFlagLogic } from 'lib/logic/featureFlagLogic' +import { FEATURE_FLAGS } from 'lib/constants' export interface NewSurvey extends Pick< @@ -175,7 +177,12 @@ export const surveyLogic = kea([ 'reportSurveyViewed', ], ], - values: [pluginsLogic, ['installedPlugins', 'loading as pluginsLoading', 'enabledPlugins']], + values: [ + pluginsLogic, + ['installedPlugins', 'loading as pluginsLoading', 'enabledPlugins'], + enabledFlagLogic, + ['featureFlags as enabledFlags'], + ], })), actions({ editingSurvey: (editing: boolean) => ({ editing }), @@ -313,12 +320,15 @@ export const surveyLogic = kea([ }, ], showSurveyAppWarning: [ - (s) => [s.survey, s.enabledPlugins, s.pluginsLoading], - (survey: Survey, enabledPlugins: PluginType[], pluginsLoading: boolean): boolean => { - return !!( - survey.type !== SurveyType.API && - !pluginsLoading && - !enabledPlugins.find((plugin) => plugin.name === 'Surveys app') + (s) => [s.survey, s.enabledPlugins, s.pluginsLoading, s.enabledFlags], + (survey: Survey, enabledPlugins: PluginType[], pluginsLoading: boolean, enabledFlags): boolean => { + return ( + !enabledFlags[FEATURE_FLAGS.SURVEYS_SITE_APP_DEPRECATION] && + !!( + survey.type !== SurveyType.API && + !pluginsLoading && + !enabledPlugins.find((plugin) => plugin.name === 'Surveys app') + ) ) }, ], diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 9c0d54b3d6733..516f61ea400be 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -343,6 +343,7 @@ export interface TeamType extends TeamBasicType { capture_console_log_opt_in: boolean capture_performance_opt_in: boolean autocapture_exceptions_opt_in: boolean + surveys_opt_in?: boolean autocapture_exceptions_errors_to_ignore: string[] test_account_filters: AnyPropertyFilter[] test_account_filters_default_checked: boolean diff --git a/posthog/api/decide.py b/posthog/api/decide.py index 880a62128abdc..66364ba617ae9 100644 --- a/posthog/api/decide.py +++ b/posthog/api/decide.py @@ -225,6 +225,8 @@ def get_decide(request: HttpRequest): "recorderVersion": "v2", } + response["surveys"] = True if team.surveys_opt_in else False + site_apps = [] # errors mean the database is unavailable, bail in this case if team.inject_web_apps and not errors: diff --git a/posthog/api/team.py b/posthog/api/team.py index 179bcb5303754..ebfd3fe2f72d3 100644 --- a/posthog/api/team.py +++ b/posthog/api/team.py @@ -90,6 +90,7 @@ class Meta: "session_recording_opt_in", "recording_domains", "inject_web_apps", + "surveys_opt_in", ] @@ -139,6 +140,7 @@ class Meta: "inject_web_apps", "extra_settings", "has_completed_onboarding_for", + "surveys_opt_in", ) read_only_fields = ( "id", From 6016bdb6d66e70ecab9411cb096ea1682d4c8839 Mon Sep 17 00:00:00 2001 From: PostHog Bot <69588470+posthog-bot@users.noreply.github.com> Date: Tue, 26 Sep 2023 09:48:30 -0400 Subject: [PATCH 20/22] chore(deps): Update posthog-js to 1.81.1 (#17625) Co-authored-by: posthog-bot --- package.json | 2 +- pnpm-lock.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index c0a5ae8b76aba..c1a3772e89de9 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "md5": "^2.3.0", "monaco-editor": "^0.39.0", "papaparse": "^5.4.1", - "posthog-js": "1.81.0", + "posthog-js": "1.81.1", "posthog-js-lite": "2.0.0-alpha5", "prettier": "^2.8.8", "prop-types": "^15.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a85e2688bac8c..74ec71a92715e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -198,8 +198,8 @@ dependencies: specifier: ^5.4.1 version: 5.4.1 posthog-js: - specifier: 1.81.0 - version: 1.81.0 + specifier: 1.81.1 + version: 1.81.1 posthog-js-lite: specifier: 2.0.0-alpha5 version: 2.0.0-alpha5 @@ -15016,8 +15016,8 @@ packages: resolution: {integrity: sha512-tlkBdypJuvK/s00n4EiQjwYVfuuZv6vt8BF3g1ooIQa2Gz9Vz80p8q3qsPLZ0V5ErGRy6i3Q4fWC9TDzR7GNRQ==} dev: false - /posthog-js@1.81.0: - resolution: {integrity: sha512-Ax+qzJQtJjViZ9jJPS3sf2vlDd/xZHVHcEik3tEOY0LXLgJZhbUymqW1Lo3sqsKRvKjHSidPsodTpm5/K63h+Q==} + /posthog-js@1.81.1: + resolution: {integrity: sha512-pQfG9ZGVn3R7Uh1cC/S02trZ6u4TOLs1NhZG3WiNrqMKDA8MJQjZ/PqdkLO0/BeozRBfIbON6pw3xfOIneIclg==} dependencies: fflate: 0.4.8 dev: false From fd93ca40254e63a8cef97c5db38c09c86ce5b3f8 Mon Sep 17 00:00:00 2001 From: Ben White Date: Tue, 26 Sep 2023 15:54:56 +0200 Subject: [PATCH 21/22] fix: Fixed up server config naming (#17598) --- .../session-recordings-consumer-v2.ts | 61 ++++++++++--------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts b/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts index 950eb20f8afcf..f783b7390bc7e 100644 --- a/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts +++ b/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer-v2.ts @@ -106,31 +106,31 @@ export class SessionRecordingIngesterV2 { partitionLockInterval: NodeJS.Timer | null = null teamsRefresher: BackgroundRefresher> offsetsRefresher: BackgroundRefresher> - recordingConsumerConfig: PluginsServerConfig + config: PluginsServerConfig topic = KAFKA_SESSION_RECORDING_SNAPSHOT_ITEM_EVENTS private promises: Set> = new Set() constructor( - private serverConfig: PluginsServerConfig, + globalServerConfig: PluginsServerConfig, private postgres: PostgresRouter, private objectStorage: ObjectStorage ) { - this.recordingConsumerConfig = sessionRecordingConsumerConfig(this.serverConfig) - this.redisPool = createRedisPool(this.recordingConsumerConfig) + // NOTE: globalServerConfig contains the default pluginServer values, typically not pointing at dedicated resources like kafka or redis + // We stil connect to some of the non-dedicated resources such as postgres or the Replay events kafka. + this.config = sessionRecordingConsumerConfig(globalServerConfig) + this.redisPool = createRedisPool(this.config) - this.realtimeManager = new RealtimeManager(this.redisPool, this.recordingConsumerConfig) - this.partitionLocker = new PartitionLocker( - this.redisPool, - this.recordingConsumerConfig.SESSION_RECORDING_REDIS_PREFIX - ) + this.realtimeManager = new RealtimeManager(this.redisPool, this.config) + this.partitionLocker = new PartitionLocker(this.redisPool, this.config.SESSION_RECORDING_REDIS_PREFIX) this.offsetHighWaterMarker = new OffsetHighWaterMarker( this.redisPool, - serverConfig.SESSION_RECORDING_REDIS_PREFIX + this.config.SESSION_RECORDING_REDIS_PREFIX ) - this.replayEventsIngester = new ReplayEventsIngester(this.serverConfig, this.offsetHighWaterMarker) + // NOTE: This is the only place where we need to use the shared server config + this.replayEventsIngester = new ReplayEventsIngester(globalServerConfig, this.offsetHighWaterMarker) this.teamsRefresher = new BackgroundRefresher(async () => { try { @@ -234,7 +234,7 @@ export class SessionRecordingIngesterV2 { const { partition, topic } = event.metadata const sessionManager = new SessionManager( - this.serverConfig, + this.config, this.objectStorage.s3, this.realtimeManager, this.offsetHighWaterMarker, @@ -339,7 +339,7 @@ export class SessionRecordingIngesterV2 { const recordingMessages: IncomingRecordingMessage[] = [] - if (this.serverConfig.SESSION_RECORDING_PARTITION_REVOKE_OPTIMIZATION) { + if (this.config.SESSION_RECORDING_PARTITION_REVOKE_OPTIMIZATION) { await this.partitionLocker.claim(messages) } @@ -389,7 +389,7 @@ export class SessionRecordingIngesterV2 { await runInstrumentedFunction({ statsKey: `recordingingester.handleEachBatch.consumeBatch`, func: async () => { - if (this.serverConfig.SESSION_RECORDING_PARALLEL_CONSUMPTION) { + if (this.config.SESSION_RECORDING_PARALLEL_CONSUMPTION) { await Promise.all(recordingMessages.map((x) => this.consume(x))) } else { for (const message of recordingMessages) { @@ -429,8 +429,13 @@ export class SessionRecordingIngesterV2 { // Currently we can't reuse any files stored on disk, so we opt to delete them all try { - rmSync(bufferFileDir(this.serverConfig.SESSION_RECORDING_LOCAL_DIRECTORY), { recursive: true, force: true }) - mkdirSync(bufferFileDir(this.serverConfig.SESSION_RECORDING_LOCAL_DIRECTORY), { recursive: true }) + rmSync(bufferFileDir(this.config.SESSION_RECORDING_LOCAL_DIRECTORY), { + recursive: true, + force: true, + }) + mkdirSync(bufferFileDir(this.config.SESSION_RECORDING_LOCAL_DIRECTORY), { + recursive: true, + }) } catch (e) { status.error('🔥', 'Failed to recreate local buffer directory', e) captureException(e) @@ -442,13 +447,13 @@ export class SessionRecordingIngesterV2 { await this.replayEventsIngester.start() - if (this.serverConfig.SESSION_RECORDING_PARTITION_REVOKE_OPTIMIZATION) { + if (this.config.SESSION_RECORDING_PARTITION_REVOKE_OPTIMIZATION) { this.partitionLockInterval = setInterval(async () => { await this.partitionLocker.claim(this.assignedTopicPartitions) }, PARTITION_LOCK_INTERVAL_MS) } - const connectionConfig = createRdConnectionConfigFromEnvVars(this.recordingConsumerConfig) + const connectionConfig = createRdConnectionConfigFromEnvVars(this.config) // Create a node-rdkafka consumer that fetches batches of messages, runs // eachBatchWithContext, then commits offsets for the batch. @@ -461,15 +466,15 @@ export class SessionRecordingIngesterV2 { // the largest size of a message that can be fetched by the consumer. // the largest size our MSK cluster allows is 20MB // we only use 9 or 10MB but there's no reason to limit this 🤷️ - consumerMaxBytes: this.recordingConsumerConfig.KAFKA_CONSUMPTION_MAX_BYTES, - consumerMaxBytesPerPartition: this.recordingConsumerConfig.KAFKA_CONSUMPTION_MAX_BYTES_PER_PARTITION, + consumerMaxBytes: this.config.KAFKA_CONSUMPTION_MAX_BYTES, + consumerMaxBytesPerPartition: this.config.KAFKA_CONSUMPTION_MAX_BYTES_PER_PARTITION, // our messages are very big, so we don't want to buffer too many - queuedMinMessages: this.recordingConsumerConfig.SESSION_RECORDING_KAFKA_QUEUE_SIZE, - consumerMaxWaitMs: this.recordingConsumerConfig.KAFKA_CONSUMPTION_MAX_WAIT_MS, - consumerErrorBackoffMs: this.recordingConsumerConfig.KAFKA_CONSUMPTION_ERROR_BACKOFF_MS, - fetchBatchSize: this.recordingConsumerConfig.SESSION_RECORDING_KAFKA_BATCH_SIZE, - batchingTimeoutMs: this.recordingConsumerConfig.KAFKA_CONSUMPTION_BATCHING_TIMEOUT_MS, - topicCreationTimeoutMs: this.recordingConsumerConfig.KAFKA_TOPIC_CREATION_TIMEOUT_MS, + queuedMinMessages: this.config.SESSION_RECORDING_KAFKA_QUEUE_SIZE, + consumerMaxWaitMs: this.config.KAFKA_CONSUMPTION_MAX_WAIT_MS, + consumerErrorBackoffMs: this.config.KAFKA_CONSUMPTION_ERROR_BACKOFF_MS, + fetchBatchSize: this.config.SESSION_RECORDING_KAFKA_BATCH_SIZE, + batchingTimeoutMs: this.config.KAFKA_CONSUMPTION_BATCHING_TIMEOUT_MS, + topicCreationTimeoutMs: this.config.KAFKA_TOPIC_CREATION_TIMEOUT_MS, autoCommit: false, eachBatch: async (messages) => { return await this.handleEachBatch(messages) @@ -548,7 +553,7 @@ export class SessionRecordingIngesterV2 { this.partitionAssignments[topicPartition.partition] = {} }) - if (this.serverConfig.SESSION_RECORDING_PARTITION_REVOKE_OPTIMIZATION) { + if (this.config.SESSION_RECORDING_PARTITION_REVOKE_OPTIMIZATION) { await this.partitionLocker.claim(topicPartitions) } await this.offsetsRefresher.refresh() @@ -595,7 +600,7 @@ export class SessionRecordingIngesterV2 { logExecutionTime: true, timeout: 30000, // same as the partition lock func: async () => { - if (this.serverConfig.SESSION_RECORDING_PARTITION_REVOKE_OPTIMIZATION) { + if (this.config.SESSION_RECORDING_PARTITION_REVOKE_OPTIMIZATION) { // Extend our claim on these partitions to give us time to flush await this.partitionLocker.claim(topicPartitions) status.info( From 705d87719a76e1693a09589f28ff95cf783de951 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Tue, 26 Sep 2023 14:59:23 +0100 Subject: [PATCH 22/22] chore: cache the instance status page for 60 seconds (#17539) * chore: cache the instance status page for 60 seconds * add a note to the system overview tab * Update UI snapshots for `chromium` (1) * clear cache between tests --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- frontend/src/scenes/instance/SystemStatus/index.tsx | 9 +++++++-- posthog/api/instance_status.py | 3 +++ posthog/settings/web.py | 1 + posthog/test/base.py | 2 ++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/frontend/src/scenes/instance/SystemStatus/index.tsx b/frontend/src/scenes/instance/SystemStatus/index.tsx index 11adb42107c21..6bee341d1df3a 100644 --- a/frontend/src/scenes/instance/SystemStatus/index.tsx +++ b/frontend/src/scenes/instance/SystemStatus/index.tsx @@ -5,7 +5,7 @@ import { systemStatusLogic, InstanceStatusTabName } from './systemStatusLogic' import { useActions, useValues } from 'kea' import { PageHeader } from 'lib/components/PageHeader' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { IconOpenInNew } from 'lib/lemon-ui/icons' +import { IconInfo, IconOpenInNew } from 'lib/lemon-ui/icons' import { OverviewTab } from 'scenes/instance/SystemStatus/OverviewTab' import { InternalMetricsTab } from 'scenes/instance/SystemStatus/InternalMetricsTab' import { SceneExport } from 'scenes/sceneTypes' @@ -17,6 +17,7 @@ import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { FEATURE_FLAGS } from 'lib/constants' import { KafkaInspectorTab } from './KafkaInspectorTab' import { LemonTab, LemonTabs } from 'lib/lemon-ui/LemonTabs' +import { Tooltip } from 'lib/lemon-ui/Tooltip' export const scene: SceneExport = { component: SystemStatus, @@ -33,7 +34,11 @@ export function SystemStatus(): JSX.Element { let tabs = [ { key: 'overview', - label: 'System overview', + label: ( + System overview is cached for 60 seconds}> + System overview + + ), content: , }, ] as LemonTab[] diff --git a/posthog/api/instance_status.py b/posthog/api/instance_status.py index 6d685dc783e32..c54b9b2bc071c 100644 --- a/posthog/api/instance_status.py +++ b/posthog/api/instance_status.py @@ -2,6 +2,8 @@ from django.conf import settings from django.db import connection +from django.utils.decorators import method_decorator +from django.views.decorators.cache import cache_page from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated @@ -33,6 +35,7 @@ class InstanceStatusViewSet(viewsets.ViewSet): permission_classes = [IsAuthenticated, SingleTenancyOrAdmin] + @method_decorator(cache_page(60)) def list(self, request: Request) -> Response: redis_alive = is_redis_alive() postgres_alive = is_postgres_alive() diff --git a/posthog/settings/web.py b/posthog/settings/web.py index ca0c035765a7e..b062ce632a71a 100644 --- a/posthog/settings/web.py +++ b/posthog/settings/web.py @@ -320,6 +320,7 @@ def add_recorder_js_headers(headers, path, url): "^/api/organizations/@current/plugins/?$", "^api/projects/@current/feature_flags/my_flags/?$", "^/?api/projects/\\d+/query/?$", + "^/?api/instance_status/?$", ] ), ) diff --git a/posthog/test/base.py b/posthog/test/base.py index 8b66387037a7c..5457bbe4056bc 100644 --- a/posthog/test/base.py +++ b/posthog/test/base.py @@ -12,6 +12,7 @@ import pytest import sqlparse from django.apps import apps +from django.core.cache import cache from django.db import connection, connections from django.db.migrations.executor import MigrationExecutor from django.test import TestCase, TransactionTestCase, override_settings @@ -232,6 +233,7 @@ class APIBaseTest(TestMixin, ErrorResponsesMixin, DRFTestCase): def setUp(self): super().setUp() + cache.clear() TEST_clear_cloud_cache(self.initial_cloud_mode) TEST_clear_instance_license_cache()