From f2522de038a091196b2c97d9e2f2c60368264b2b Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Wed, 11 Sep 2024 14:05:56 +0200 Subject: [PATCH] feat(environments): Add new environments endpoint and move /projects to Project (#24154) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .storybook/app-context.ts | 3 +- ee/api/test/test_billing.py | 1 + ee/api/test/test_organization.py | 2 +- ee/api/test/test_project.py | 48 + ee/api/test/test_team.py | 998 +++++---- ee/clickhouse/views/experiments.py | 2 +- ee/clickhouse/views/groups.py | 4 +- ee/clickhouse/views/insights.py | 2 +- ee/urls.py | 26 +- ...ights--funnel-left-to-right-edit--dark.png | Bin 155281 -> 154427 bytes frontend/src/lib/api.mock.ts | 8 + frontend/src/types.ts | 7 + posthog/api/__init__.py | 170 +- posthog/api/project.py | 508 +++++ posthog/api/routing.py | 28 +- posthog/api/shared.py | 113 +- posthog/api/signup.py | 2 +- posthog/api/team.py | 161 +- .../api/test/__snapshots__/test_api_docs.ambr | 94 +- .../api/test/__snapshots__/test_decide.ambr | 387 +++- .../api/test/__snapshots__/test_event.ambr | 48 +- posthog/api/test/test_decide.py | 1 + posthog/api/test/test_project.py | 11 + posthog/api/test/test_routing.py | 67 +- posthog/api/test/test_team.py | 1950 +++++++++-------- posthog/demo/legacy/__init__.py | 15 - .../test/__snapshots__/test_trends.ambr | 398 +++- .../models/activity_logging/activity_log.py | 7 +- posthog/models/organization.py | 4 +- posthog/models/project.py | 28 +- posthog/models/team/team.py | 79 +- posthog/models/test/test_project.py | 6 +- posthog/models/user.py | 4 +- posthog/permissions.py | 6 +- .../test/__snapshots__/test_trends.ambr | 116 +- posthog/test/base.py | 27 +- posthog/test/test_middleware.py | 2 +- posthog/test/test_team.py | 18 +- posthog/urls.py | 12 +- posthog/utils.py | 8 + .../api/test/test_external_data_source.py | 4 +- 41 files changed, 3495 insertions(+), 1880 deletions(-) create mode 100644 ee/api/test/test_project.py create mode 100644 posthog/api/project.py create mode 100644 posthog/api/test/test_project.py diff --git a/.storybook/app-context.ts b/.storybook/app-context.ts index 6f52182cec58c..a85f06aa80e79 100644 --- a/.storybook/app-context.ts +++ b/.storybook/app-context.ts @@ -1,4 +1,4 @@ -import { MOCK_DEFAULT_TEAM } from 'lib/api.mock' +import { MOCK_DEFAULT_TEAM, MOCK_DEFAULT_PROJECT } from 'lib/api.mock' import { AppContext } from '~/types' export const getStorybookAppContext = (): AppContext => ({ @@ -6,6 +6,7 @@ export const getStorybookAppContext = (): AppContext => ({ // Ideally we wouldn't set `current_team` here, the same way we don't set `current_user`, but unfortunately // as of March 2024, a bunch of logics make the assumption that this is set, via `AppConfig` current_team: MOCK_DEFAULT_TEAM, + current_project: MOCK_DEFAULT_PROJECT, current_user: undefined as any, // `undefined` triggers a fetch and lets us mock the data default_event_name: '$pageview', persisted_feature_flags: [], diff --git a/ee/api/test/test_billing.py b/ee/api/test/test_billing.py index 6062516c4ccef..a053dd1ac9c1c 100644 --- a/ee/api/test/test_billing.py +++ b/ee/api/test/test_billing.py @@ -777,6 +777,7 @@ def mock_implementation(url: str, headers: Any = None, params: Any = None) -> Ma # Create a demo project self.organization_membership.level = OrganizationMembership.Level.ADMIN self.organization_membership.save() + self.assertEqual(Team.objects.count(), 1) response = self.client.post("/api/projects/", {"name": "Test", "is_demo": True}) self.assertEqual(response.status_code, 201) self.assertEqual(Team.objects.count(), 3) diff --git a/ee/api/test/test_organization.py b/ee/api/test/test_organization.py index ca0d68413cf4c..ed47558d1efc4 100644 --- a/ee/api/test/test_organization.py +++ b/ee/api/test/test_organization.py @@ -28,7 +28,7 @@ def test_create_organization(self): OrganizationMembership.Level.OWNER, ) - @patch("secrets.choice", return_value="Y") + @patch("posthog.models.utils.generate_random_short_suffix", return_value="YYYY") def test_create_two_similarly_named_organizations(self, mock_choice): response = self.client.post( "/api/organizations/", diff --git a/ee/api/test/test_project.py b/ee/api/test/test_project.py new file mode 100644 index 0000000000000..5061bad71d32e --- /dev/null +++ b/ee/api/test/test_project.py @@ -0,0 +1,48 @@ +from ee.api.test.test_team import team_enterprise_api_test_factory +from posthog.api.test.test_team import EnvironmentToProjectRewriteClient +from posthog.models.organization import Organization, OrganizationMembership +from posthog.models.project import Project +from posthog.models.team.team import Team + + +class TestProjectEnterpriseAPI(team_enterprise_api_test_factory()): + """ + We inherit from TestTeamEnterpriseAPI, as previously /api/projects/ referred to the Team model, which used to mean "project". + Now as Team means "environment" and Project is separate, we must ensure backward compatibility of /api/projects/. + At the same time, this class is where we can continue adding `Project`-specific API tests. + """ + + client_class = EnvironmentToProjectRewriteClient + + def test_user_create_project_for_org_via_url(self): + # Set both current and new org to high enough membership level + self.organization_membership.level = OrganizationMembership.Level.ADMIN + self.organization_membership.save() + + current_org, _, _ = Organization.objects.bootstrap(self.user, name="other_org") + other_org = self.organization # Bootstrapping above sets it to the current org + assert Team.objects.count() == 2 + assert Project.objects.count() == 2 + + assert current_org.id == self.user.current_organization_id + response = self.client.post(f"/api/organizations/{current_org.id}/projects/", {"name": "Via current org"}) + self.assertEqual(response.status_code, 201) + assert response.json()["organization"] == str(current_org.id) + assert Team.objects.count() == 3 + assert Project.objects.count() == 3 + + assert other_org.id != self.user.current_organization_id + response = self.client.post(f"/api/organizations/{other_org.id}/projects/", {"name": "Via path org"}) + self.assertEqual(response.status_code, 201, msg=response.json()) + assert response.json()["organization"] == str(other_org.id) + assert Team.objects.count() == 4 + assert Project.objects.count() == 4 + + def test_user_cannot_create_project_in_org_without_access(self): + _, _, _ = Organization.objects.bootstrap(self.user, name="other_org") + other_org = self.organization # Bootstrapping above sets it to the current org + + assert other_org.id != self.user.current_organization_id + response = self.client.post(f"/api/organizations/{other_org.id}/projects/", {"name": "Via path org"}) + self.assertEqual(response.status_code, 403, msg=response.json()) + assert response.json() == self.permission_denied_response("Your organization access level is insufficient.") diff --git a/ee/api/test/test_team.py b/ee/api/test/test_team.py index db9fb7efdbf37..d90f699fcd5c6 100644 --- a/ee/api/test/test_team.py +++ b/ee/api/test/test_team.py @@ -8,519 +8,503 @@ from ee.api.test.base import APILicensedTest from ee.models.explicit_team_membership import ExplicitTeamMembership from posthog.models.organization import Organization, OrganizationMembership +from posthog.models.project import Project from posthog.models.team import Team from posthog.models.user import User from posthog.test.base import FuzzyInt -class TestProjectEnterpriseAPI(APILicensedTest): - CLASS_DATA_LEVEL_SETUP = False - - # Creating projects - - def test_create_project(self): - self.organization_membership.level = OrganizationMembership.Level.ADMIN - self.organization_membership.save() - response = self.client.post("/api/projects/", {"name": "Test"}) - self.assertEqual(response.status_code, 201) - self.assertEqual(Team.objects.count(), 2) - response_data = response.json() - self.assertDictContainsSubset( - { - "name": "Test", - "access_control": False, - "effective_membership_level": OrganizationMembership.Level.ADMIN, - }, - response_data, - ) - self.assertEqual(self.organization.teams.count(), 2) - - def test_non_admin_cannot_create_project(self): - self.organization_membership.level = OrganizationMembership.Level.MEMBER - self.organization_membership.save() - count = Team.objects.count() - response = self.client.post("/api/projects/", {"name": "Test"}) - self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.assertEqual(Team.objects.count(), count) - self.assertEqual( - response.json(), - self.permission_denied_response("Your organization access level is insufficient."), - ) - - def test_create_demo_project(self, *args): - self.organization_membership.level = OrganizationMembership.Level.ADMIN - self.organization_membership.save() - response = self.client.post("/api/projects/", {"name": "Hedgebox", "is_demo": True}) - self.assertEqual(Team.objects.count(), 3) - self.assertEqual(response.status_code, 201) - response_data = response.json() - self.assertDictContainsSubset( - { - "name": "Hedgebox", - "access_control": False, - "effective_membership_level": OrganizationMembership.Level.ADMIN, - }, - response_data, - ) - self.assertEqual(self.organization.teams.count(), 2) - - def test_create_two_demo_projects(self, *args): - self.organization_membership.level = OrganizationMembership.Level.ADMIN - self.organization_membership.save() - response = self.client.post("/api/projects/", {"name": "Hedgebox", "is_demo": True}) - self.assertEqual(Team.objects.count(), 3) - self.assertEqual(response.status_code, 201) - response_data = response.json() - self.assertDictContainsSubset( - { - "name": "Hedgebox", - "access_control": False, - "effective_membership_level": OrganizationMembership.Level.ADMIN, - }, - response_data, - ) - response_2 = self.client.post("/api/projects/", {"name": "Hedgebox", "is_demo": True}) - self.assertEqual(Team.objects.count(), 3) - response_2_data = response_2.json() - self.assertDictContainsSubset( - { - "type": "authentication_error", - "code": "permission_denied", - "detail": "You must upgrade your PostHog plan to be able to create and manage multiple projects.", - }, - response_2_data, - ) - self.assertEqual(self.organization.teams.count(), 2) - - def test_user_that_does_not_belong_to_an_org_cannot_create_a_project(self): - user = User.objects.create(email="no_org@posthog.com") - self.client.force_login(user) - - response = self.client.post("/api/projects/", {"name": "Test"}) - self.assertEqual(response.status_code, HTTP_404_NOT_FOUND, response.content) - self.assertEqual( - response.json(), - { - "type": "invalid_request", - "code": "not_found", - "detail": "You need to belong to an organization.", - "attr": None, - }, - ) - - def test_user_create_project_for_org_via_url(self): - # Set both current and new org to high enough membership level - self.organization_membership.level = OrganizationMembership.Level.ADMIN - self.organization_membership.save() - - current_org, _, _ = Organization.objects.bootstrap(self.user, name="other_org") - other_org = self.organization # Bootstrapping above sets it to the current org - - assert current_org.id == self.user.current_organization_id - response = self.client.post(f"/api/organizations/{current_org.id}/projects/", {"name": "Via current org"}) - self.assertEqual(response.status_code, 201) - assert response.json()["organization"] == str(current_org.id) - - assert other_org.id != self.user.current_organization_id - response = self.client.post(f"/api/organizations/{other_org.id}/projects/", {"name": "Via path org"}) - self.assertEqual(response.status_code, 201, msg=response.json()) - assert response.json()["organization"] == str(other_org.id) - - def test_user_cannot_create_project_in_org_without_access(self): - _, _, _ = Organization.objects.bootstrap(self.user, name="other_org") - other_org = self.organization # Bootstrapping above sets it to the current org - - assert other_org.id != self.user.current_organization_id - response = self.client.post(f"/api/organizations/{other_org.id}/projects/", {"name": "Via path org"}) - self.assertEqual(response.status_code, 403, msg=response.json()) - assert response.json() == self.permission_denied_response("Your organization access level is insufficient.") - - # Deleting projects - - def test_delete_team_as_org_admin_allowed(self): - self.organization_membership.level = OrganizationMembership.Level.ADMIN - self.organization_membership.save() - response = self.client.delete(f"/api/projects/{self.team.id}") - self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) - self.assertEqual(Team.objects.filter(organization=self.organization).count(), 0) - - def test_delete_team_as_org_member_forbidden(self): - self.organization_membership.level = OrganizationMembership.Level.MEMBER - self.organization_membership.save() - response = self.client.delete(f"/api/projects/{self.team.id}") - self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.assertEqual(Team.objects.filter(organization=self.organization).count(), 1) - - def test_delete_open_team_as_org_member_but_project_admin_forbidden(self): - self.organization_membership.level = OrganizationMembership.Level.MEMBER - self.organization_membership.save() - ExplicitTeamMembership.objects.create( - team=self.team, - parent_membership=self.organization_membership, - level=ExplicitTeamMembership.Level.ADMIN, - ) - response = self.client.delete(f"/api/projects/{self.team.id}") - self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.assertEqual(Team.objects.filter(organization=self.organization).count(), 1) - - def test_delete_private_team_as_org_member_but_project_admin_allowed(self): - self.organization_membership.level = OrganizationMembership.Level.MEMBER - self.organization_membership.save() - self.team.access_control = True - self.team.save() - ExplicitTeamMembership.objects.create( - team=self.team, - parent_membership=self.organization_membership, - level=ExplicitTeamMembership.Level.ADMIN, - ) - response = self.client.delete(f"/api/projects/{self.team.id}") - self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) - self.assertEqual(Team.objects.filter(organization=self.organization).count(), 0) - - def test_delete_second_team_as_org_admin_allowed(self): - self.organization_membership.level = OrganizationMembership.Level.ADMIN - self.organization_membership.save() - team = Team.objects.create(organization=self.organization) - response = self.client.delete(f"/api/projects/{team.id}") - self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) - self.assertEqual(Team.objects.filter(organization=self.organization).count(), 1) - - def test_no_delete_team_not_administrating_organization(self): - self.organization_membership.level = OrganizationMembership.Level.MEMBER - self.organization_membership.save() - team = Team.objects.create(organization=self.organization) - response = self.client.delete(f"/api/projects/{team.id}") - self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.assertEqual(Team.objects.filter(organization=self.organization).count(), 2) - - def test_no_delete_team_not_belonging_to_organization(self): - team_1 = Organization.objects.bootstrap(None)[2] - response = self.client.delete(f"/api/projects/{team_1.id}") - self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) - self.assertTrue(Team.objects.filter(id=team_1.id).exists()) - organization, _, _ = User.objects.bootstrap("X", "someone@x.com", "qwerty", "Someone") - team_2 = Team.objects.create(organization=organization) - response = self.client.delete(f"/api/projects/{team_2.id}") - self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) - self.assertEqual(Team.objects.filter(organization=organization).count(), 2) - - # Updating projects - - def test_rename_project_as_org_member_allowed(self): - self.organization_membership.level = OrganizationMembership.Level.MEMBER - self.organization_membership.save() - - response = self.client.patch(f"/api/projects/@current/", {"name": "Erinaceus europaeus"}) - self.team.refresh_from_db() - - self.assertEqual(response.status_code, HTTP_200_OK) - self.assertEqual(self.team.name, "Erinaceus europaeus") - - def test_rename_private_project_as_org_member_forbidden(self): - self.organization_membership.level = OrganizationMembership.Level.MEMBER - self.organization_membership.save() - self.team.access_control = True - self.team.save() - - response = self.client.patch(f"/api/projects/@current/", {"name": "Acherontia atropos"}) - self.team.refresh_from_db() - - self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.assertEqual(self.team.name, "Default project") - - def test_rename_private_project_current_as_org_outsider_forbidden(self): - self.organization_membership.delete() - - response = self.client.patch(f"/api/projects/@current/", {"name": "Acherontia atropos"}) - self.team.refresh_from_db() - - self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) - - def test_rename_private_project_id_as_org_outsider_forbidden(self): - self.organization_membership.delete() - - response = self.client.patch(f"/api/projects/{self.team.id}/", {"name": "Acherontia atropos"}) - self.team.refresh_from_db() - - self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) - - def test_rename_private_project_as_org_member_and_project_member_allowed(self): - self.organization_membership.level = OrganizationMembership.Level.MEMBER - self.organization_membership.save() - self.team.access_control = True - self.team.save() - ExplicitTeamMembership.objects.create( - team=self.team, - parent_membership=self.organization_membership, - level=ExplicitTeamMembership.Level.MEMBER, - ) - - response = self.client.patch(f"/api/projects/@current/", {"name": "Acherontia atropos"}) - self.team.refresh_from_db() - - self.assertEqual(response.status_code, HTTP_200_OK) - self.assertEqual(self.team.name, "Acherontia atropos") - - def test_enable_access_control_as_org_member_forbidden(self): - self.organization_membership.level = OrganizationMembership.Level.MEMBER - self.organization_membership.save() - - response = self.client.patch(f"/api/projects/@current/", {"access_control": True}) - self.team.refresh_from_db() - - self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.assertFalse(self.team.access_control) - - def test_enable_access_control_as_org_admin_allowed(self): - self.organization_membership.level = OrganizationMembership.Level.ADMIN - self.organization_membership.save() - - response = self.client.patch(f"/api/projects/@current/", {"access_control": True}) - self.team.refresh_from_db() - - self.assertEqual(response.status_code, HTTP_200_OK) - self.assertTrue(self.team.access_control) - - def test_enable_access_control_as_org_member_and_project_admin_forbidden(self): - self.organization_membership.level = OrganizationMembership.Level.MEMBER - self.organization_membership.save() - ExplicitTeamMembership.objects.create( - team=self.team, - parent_membership=self.organization_membership, - level=ExplicitTeamMembership.Level.ADMIN, - ) - - response = self.client.patch(f"/api/projects/@current/", {"access_control": True}) - self.team.refresh_from_db() - - self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.assertFalse(self.team.access_control) - - def test_disable_access_control_as_org_member_forbidden(self): - self.organization_membership.level = OrganizationMembership.Level.MEMBER - self.organization_membership.save() - self.team.access_control = True - self.team.save() - - response = self.client.patch(f"/api/projects/@current/", {"access_control": False}) - self.team.refresh_from_db() - - self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.assertTrue(self.team.access_control) - - def test_disable_access_control_as_org_member_and_project_admin_forbidden(self): - # Only org-wide admins+ should be allowed to make the project open, - # because if a project-specific admin who is only an org member did it, they wouldn't be able to reenable it - self.organization_membership.level = OrganizationMembership.Level.MEMBER - self.organization_membership.save() - self.team.access_control = True - self.team.save() - ExplicitTeamMembership.objects.create( - team=self.team, - parent_membership=self.organization_membership, - level=ExplicitTeamMembership.Level.ADMIN, - ) - - response = self.client.patch(f"/api/projects/@current/", {"access_control": False}) - self.team.refresh_from_db() - - self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.assertTrue(self.team.access_control) - - def test_disable_access_control_as_org_admin_allowed(self): - self.organization_membership.level = OrganizationMembership.Level.ADMIN - self.organization_membership.save() - self.team.access_control = True - self.team.save() - - response = self.client.patch(f"/api/projects/@current/", {"access_control": False}) - self.team.refresh_from_db() - - self.assertEqual(response.status_code, HTTP_200_OK) - self.assertFalse(self.team.access_control) - - def test_can_update_and_retrieve_person_property_names_excluded_from_correlation(self): - response = self.client.patch( - f"/api/projects/@current/", - {"correlation_config": {"excluded_person_property_names": ["$os"]}}, - ) - self.assertEqual(response.status_code, HTTP_200_OK) - - response = self.client.get(f"/api/projects/@current/") - self.assertEqual(response.status_code, HTTP_200_OK) - - response_data = response.json() - - self.assertDictContainsSubset( - {"correlation_config": {"excluded_person_property_names": ["$os"]}}, - response_data, - ) - - # Fetching projects - - def test_fetch_team_as_org_admin_works(self): - self.organization_membership.level = OrganizationMembership.Level.ADMIN - self.organization_membership.save() - - response = self.client.get(f"/api/projects/@current/") - response_data = response.json() - - self.assertEqual(response.status_code, HTTP_200_OK) - self.assertDictContainsSubset( - { - "name": "Default project", - "access_control": False, - "effective_membership_level": OrganizationMembership.Level.ADMIN, - }, - response_data, - ) - - def test_fetch_team_as_org_member_works(self): - self.organization_membership.level = OrganizationMembership.Level.MEMBER - self.organization_membership.save() - - response = self.client.get(f"/api/projects/@current/") - response_data = response.json() - - self.assertEqual(response.status_code, HTTP_200_OK) - self.assertDictContainsSubset( - { - "name": "Default project", - "access_control": False, - "effective_membership_level": OrganizationMembership.Level.MEMBER, - }, - response_data, - ) - - def test_fetch_private_team_as_org_member(self): - self.organization_membership.level = OrganizationMembership.Level.MEMBER - self.organization_membership.save() - self.team.access_control = True - self.team.save() - - response = self.client.get(f"/api/projects/@current/") - response_data = response.json() - - self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.assertEqual( - self.permission_denied_response("You don't have sufficient permissions in the project."), - response_data, - ) - - def test_fetch_private_team_as_org_member_and_project_member(self): - self.organization_membership.level = OrganizationMembership.Level.MEMBER - self.organization_membership.save() - self.team.access_control = True - self.team.save() - ExplicitTeamMembership.objects.create( - team=self.team, - parent_membership=self.organization_membership, - level=ExplicitTeamMembership.Level.MEMBER, - ) - - response = self.client.get(f"/api/projects/@current/") - response_data = response.json() - - self.assertEqual(response.status_code, HTTP_200_OK) - self.assertDictContainsSubset( - { - "name": "Default project", - "access_control": True, - "effective_membership_level": OrganizationMembership.Level.MEMBER, - }, - response_data, - ) - - def test_fetch_private_team_as_org_member_and_project_admin(self): - self.organization_membership.level = OrganizationMembership.Level.MEMBER - self.organization_membership.save() - self.team.access_control = True - self.team.save() - ExplicitTeamMembership.objects.create( - team=self.team, - parent_membership=self.organization_membership, - level=ExplicitTeamMembership.Level.ADMIN, - ) - - response = self.client.get(f"/api/projects/@current/") - response_data = response.json() - - self.assertEqual(response.status_code, HTTP_200_OK) - self.assertDictContainsSubset( - { - "name": "Default project", - "access_control": True, - "effective_membership_level": OrganizationMembership.Level.ADMIN, - }, - response_data, - ) - - def test_fetch_team_as_org_outsider(self): - self.organization_membership.delete() - response = self.client.get(f"/api/projects/@current/") - response_data = response.json() - - self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) - self.assertEqual(self.not_found_response(), response_data) - - def test_fetch_nonexistent_team(self): - response = self.client.get(f"/api/projects/234444/") - response_data = response.json() - - self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) - self.assertEqual(self.not_found_response(), response_data) - - def test_list_teams_restricted_ones_hidden(self): - self.organization_membership.level = OrganizationMembership.Level.MEMBER - self.organization_membership.save() - Team.objects.create( - organization=self.organization, - name="Other", - access_control=True, - ) - - # The other team should not be returned as it's restricted for the logged-in user - projects_response = self.client.get(f"/api/projects/") - - # 9 (above): - with self.assertNumQueries(FuzzyInt(9, 10)): - current_org_response = self.client.get(f"/api/organizations/{self.organization.id}/") - - self.assertEqual(projects_response.status_code, HTTP_200_OK) - self.assertEqual( - projects_response.json().get("results"), - [ +def team_enterprise_api_test_factory(): # type: ignore + class TestTeamEnterpriseAPI(APILicensedTest): + CLASS_DATA_LEVEL_SETUP = False + + # Creating projects + + def test_create_team(self): + self.organization_membership.level = OrganizationMembership.Level.ADMIN + self.organization_membership.save() + self.assertEqual(Team.objects.count(), 1) + self.assertEqual(Project.objects.count(), 1) + response = self.client.post("/api/environments/", {"name": "Test"}) + self.assertEqual(response.status_code, 201) + self.assertEqual(Team.objects.count(), 2) + self.assertEqual(Project.objects.count(), 2) + response_data = response.json() + self.assertDictContainsSubset( + { + "name": "Test", + "access_control": False, + "effective_membership_level": OrganizationMembership.Level.ADMIN, + }, + response_data, + ) + self.assertEqual(self.organization.teams.count(), 2) + + def test_non_admin_cannot_create_team(self): + self.organization_membership.level = OrganizationMembership.Level.MEMBER + self.organization_membership.save() + count = Team.objects.count() + response = self.client.post("/api/environments/", {"name": "Test"}) + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) + self.assertEqual(Team.objects.count(), count) + self.assertEqual( + response.json(), + self.permission_denied_response("Your organization access level is insufficient."), + ) + + def test_create_demo_team(self, *args): + self.organization_membership.level = OrganizationMembership.Level.ADMIN + self.organization_membership.save() + response = self.client.post("/api/environments/", {"name": "Hedgebox", "is_demo": True}) + self.assertEqual(Team.objects.count(), 3) + self.assertEqual(response.status_code, 201) + response_data = response.json() + self.assertDictContainsSubset( { - "id": self.team.id, - "uuid": str(self.team.uuid), - "organization": str(self.organization.id), - "api_token": self.team.api_token, - "name": self.team.name, - "completed_snippet_onboarding": False, - "has_completed_onboarding_for": {"product_analytics": True}, - "ingested_event": False, - "is_demo": False, - "timezone": "UTC", + "name": "Hedgebox", "access_control": False, - } - ], - ) - self.assertEqual(current_org_response.status_code, HTTP_200_OK) - self.assertEqual( - current_org_response.json().get("teams"), - [ + "effective_membership_level": OrganizationMembership.Level.ADMIN, + }, + response_data, + ) + self.assertEqual(self.organization.teams.count(), 2) + + def test_create_two_demo_teams(self, *args): + self.organization_membership.level = OrganizationMembership.Level.ADMIN + self.organization_membership.save() + response = self.client.post("/api/environments/", {"name": "Hedgebox", "is_demo": True}) + self.assertEqual(Team.objects.count(), 3) + self.assertEqual(response.status_code, 201) + response_data = response.json() + self.assertDictContainsSubset( { - "id": self.team.id, - "uuid": str(self.team.uuid), - "organization": str(self.organization.id), - "api_token": self.team.api_token, - "name": self.team.name, - "completed_snippet_onboarding": False, - "has_completed_onboarding_for": {"product_analytics": True}, - "ingested_event": False, - "is_demo": False, - "timezone": "UTC", + "name": "Hedgebox", "access_control": False, - } - ], - ) + "effective_membership_level": OrganizationMembership.Level.ADMIN, + }, + response_data, + ) + response_2 = self.client.post("/api/environments/", {"name": "Hedgebox", "is_demo": True}) + self.assertEqual(Team.objects.count(), 3) + response_2_data = response_2.json() + self.assertDictContainsSubset( + { + "type": "authentication_error", + "code": "permission_denied", + "detail": "You must upgrade your PostHog plan to be able to create and manage multiple projects or environments.", + }, + response_2_data, + ) + self.assertEqual(self.organization.teams.count(), 2) + + def test_user_that_does_not_belong_to_an_org_cannot_create_a_team(self): + user = User.objects.create(email="no_org@posthog.com") + self.client.force_login(user) + + response = self.client.post("/api/environments/", {"name": "Test"}) + self.assertEqual(response.status_code, HTTP_404_NOT_FOUND, response.content) + self.assertEqual( + response.json(), + { + "type": "invalid_request", + "code": "not_found", + "detail": "You need to belong to an organization.", + "attr": None, + }, + ) + + # Deleting projects + + def test_delete_team_as_org_admin_allowed(self): + self.organization_membership.level = OrganizationMembership.Level.ADMIN + self.organization_membership.save() + response = self.client.delete(f"/api/environments/{self.team.id}") + self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) + self.assertEqual(Team.objects.filter(organization=self.organization).count(), 0) + + def test_delete_team_as_org_member_forbidden(self): + self.organization_membership.level = OrganizationMembership.Level.MEMBER + self.organization_membership.save() + response = self.client.delete(f"/api/environments/{self.team.id}") + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) + self.assertEqual(Team.objects.filter(organization=self.organization).count(), 1) + + def test_delete_open_team_as_org_member_but_team_admin_forbidden(self): + self.organization_membership.level = OrganizationMembership.Level.MEMBER + self.organization_membership.save() + ExplicitTeamMembership.objects.create( + team=self.team, + parent_membership=self.organization_membership, + level=ExplicitTeamMembership.Level.ADMIN, + ) + response = self.client.delete(f"/api/environments/{self.team.id}") + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) + self.assertEqual(Team.objects.filter(organization=self.organization).count(), 1) + + def test_delete_private_team_as_org_member_but_team_admin_allowed(self): + self.organization_membership.level = OrganizationMembership.Level.MEMBER + self.organization_membership.save() + self.team.access_control = True + self.team.save() + ExplicitTeamMembership.objects.create( + team=self.team, + parent_membership=self.organization_membership, + level=ExplicitTeamMembership.Level.ADMIN, + ) + response = self.client.delete(f"/api/environments/{self.team.id}") + self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) + self.assertEqual(Team.objects.filter(organization=self.organization).count(), 0) + + def test_delete_second_team_as_org_admin_allowed(self): + self.organization_membership.level = OrganizationMembership.Level.ADMIN + self.organization_membership.save() + team = Team.objects.create(organization=self.organization) + response = self.client.delete(f"/api/environments/{team.id}") + self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) + self.assertEqual(Team.objects.filter(organization=self.organization).count(), 1) + + def test_no_delete_team_not_administrating_organization(self): + self.organization_membership.level = OrganizationMembership.Level.MEMBER + self.organization_membership.save() + team = Team.objects.create(organization=self.organization) + response = self.client.delete(f"/api/environments/{team.id}") + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) + self.assertEqual(Team.objects.filter(organization=self.organization).count(), 2) + + def test_no_delete_team_not_belonging_to_organization(self): + team_1 = Organization.objects.bootstrap(None)[2] + response = self.client.delete(f"/api/environments/{team_1.id}") + self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) + self.assertTrue(Team.objects.filter(id=team_1.id).exists()) + organization, _, _ = User.objects.bootstrap("X", "someone@x.com", "qwerty", "Someone") + team_2 = Team.objects.create(organization=organization) + response = self.client.delete(f"/api/environments/{team_2.id}") + self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) + self.assertEqual(Team.objects.filter(organization=organization).count(), 2) + + # Updating projects + + def test_rename_team_as_org_member_allowed(self): + self.organization_membership.level = OrganizationMembership.Level.MEMBER + self.organization_membership.save() + + response = self.client.patch(f"/api/environments/@current/", {"name": "Erinaceus europaeus"}) + self.team.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(self.team.name, "Erinaceus europaeus") + + def test_rename_private_team_as_org_member_forbidden(self): + self.organization_membership.level = OrganizationMembership.Level.MEMBER + self.organization_membership.save() + self.team.access_control = True + self.team.save() + + response = self.client.patch(f"/api/environments/@current/", {"name": "Acherontia atropos"}) + self.team.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) + self.assertEqual(self.team.name, "Default project") + + def test_rename_private_team_current_as_org_outsider_forbidden(self): + self.organization_membership.delete() + + response = self.client.patch(f"/api/environments/@current/", {"name": "Acherontia atropos"}) + self.team.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) + + def test_rename_private_team_id_as_org_outsider_forbidden(self): + self.organization_membership.delete() + + response = self.client.patch(f"/api/environments/{self.team.id}/", {"name": "Acherontia atropos"}) + self.team.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) + + def test_rename_private_team_as_org_member_and_team_member_allowed(self): + self.organization_membership.level = OrganizationMembership.Level.MEMBER + self.organization_membership.save() + self.team.access_control = True + self.team.save() + ExplicitTeamMembership.objects.create( + team=self.team, + parent_membership=self.organization_membership, + level=ExplicitTeamMembership.Level.MEMBER, + ) + + response = self.client.patch(f"/api/environments/@current/", {"name": "Acherontia atropos"}) + self.team.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(self.team.name, "Acherontia atropos") + + def test_enable_access_control_as_org_member_forbidden(self): + self.organization_membership.level = OrganizationMembership.Level.MEMBER + self.organization_membership.save() + + response = self.client.patch(f"/api/environments/@current/", {"access_control": True}) + self.team.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) + self.assertFalse(self.team.access_control) + + def test_enable_access_control_as_org_admin_allowed(self): + self.organization_membership.level = OrganizationMembership.Level.ADMIN + self.organization_membership.save() + + response = self.client.patch(f"/api/environments/@current/", {"access_control": True}) + self.team.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertTrue(self.team.access_control) + + def test_enable_access_control_as_org_member_and_team_admin_forbidden(self): + self.organization_membership.level = OrganizationMembership.Level.MEMBER + self.organization_membership.save() + ExplicitTeamMembership.objects.create( + team=self.team, + parent_membership=self.organization_membership, + level=ExplicitTeamMembership.Level.ADMIN, + ) + + response = self.client.patch(f"/api/environments/@current/", {"access_control": True}) + self.team.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) + self.assertFalse(self.team.access_control) + + def test_disable_access_control_as_org_member_forbidden(self): + self.organization_membership.level = OrganizationMembership.Level.MEMBER + self.organization_membership.save() + self.team.access_control = True + self.team.save() + + response = self.client.patch(f"/api/environments/@current/", {"access_control": False}) + self.team.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) + self.assertTrue(self.team.access_control) + + def test_disable_access_control_as_org_member_and_team_admin_forbidden(self): + # Only org-wide admins+ should be allowed to make the project open, + # because if a project-specific admin who is only an org member did it, they wouldn't be able to reenable it + self.organization_membership.level = OrganizationMembership.Level.MEMBER + self.organization_membership.save() + self.team.access_control = True + self.team.save() + ExplicitTeamMembership.objects.create( + team=self.team, + parent_membership=self.organization_membership, + level=ExplicitTeamMembership.Level.ADMIN, + ) + + response = self.client.patch(f"/api/environments/@current/", {"access_control": False}) + self.team.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) + self.assertTrue(self.team.access_control) + + def test_disable_access_control_as_org_admin_allowed(self): + self.organization_membership.level = OrganizationMembership.Level.ADMIN + self.organization_membership.save() + self.team.access_control = True + self.team.save() + + response = self.client.patch(f"/api/environments/@current/", {"access_control": False}) + self.team.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertFalse(self.team.access_control) + + def test_can_update_and_retrieve_person_property_names_excluded_from_correlation(self): + response = self.client.patch( + f"/api/environments/@current/", + {"correlation_config": {"excluded_person_property_names": ["$os"]}}, + ) + self.assertEqual(response.status_code, HTTP_200_OK) + + response = self.client.get(f"/api/environments/@current/") + self.assertEqual(response.status_code, HTTP_200_OK) + + response_data = response.json() + + self.assertDictContainsSubset( + {"correlation_config": {"excluded_person_property_names": ["$os"]}}, + response_data, + ) + + # Fetching projects + + def test_fetch_team_as_org_admin_works(self): + self.organization_membership.level = OrganizationMembership.Level.ADMIN + self.organization_membership.save() + + response = self.client.get(f"/api/environments/@current/") + response_data = response.json() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertDictContainsSubset( + { + "name": "Default project", + "access_control": False, + "effective_membership_level": OrganizationMembership.Level.ADMIN, + }, + response_data, + ) + + def test_fetch_team_as_org_member_works(self): + self.organization_membership.level = OrganizationMembership.Level.MEMBER + self.organization_membership.save() + + response = self.client.get(f"/api/environments/@current/") + response_data = response.json() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertDictContainsSubset( + { + "name": "Default project", + "access_control": False, + "effective_membership_level": OrganizationMembership.Level.MEMBER, + }, + response_data, + ) + + def test_fetch_private_team_as_org_member(self): + self.organization_membership.level = OrganizationMembership.Level.MEMBER + self.organization_membership.save() + self.team.access_control = True + self.team.save() + + response = self.client.get(f"/api/environments/@current/") + response_data = response.json() + + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) + self.assertEqual( + self.permission_denied_response("You don't have sufficient permissions in the project."), + response_data, + ) + + def test_fetch_private_team_as_org_member_and_team_member(self): + self.organization_membership.level = OrganizationMembership.Level.MEMBER + self.organization_membership.save() + self.team.access_control = True + self.team.save() + ExplicitTeamMembership.objects.create( + team=self.team, + parent_membership=self.organization_membership, + level=ExplicitTeamMembership.Level.MEMBER, + ) + + response = self.client.get(f"/api/environments/@current/") + response_data = response.json() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertDictContainsSubset( + { + "name": "Default project", + "access_control": True, + "effective_membership_level": OrganizationMembership.Level.MEMBER, + }, + response_data, + ) + + def test_fetch_private_team_as_org_member_and_team_admin(self): + self.organization_membership.level = OrganizationMembership.Level.MEMBER + self.organization_membership.save() + self.team.access_control = True + self.team.save() + ExplicitTeamMembership.objects.create( + team=self.team, + parent_membership=self.organization_membership, + level=ExplicitTeamMembership.Level.ADMIN, + ) + + response = self.client.get(f"/api/environments/@current/") + response_data = response.json() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertDictContainsSubset( + { + "name": "Default project", + "access_control": True, + "effective_membership_level": OrganizationMembership.Level.ADMIN, + }, + response_data, + ) + + def test_fetch_team_as_org_outsider(self): + self.organization_membership.delete() + response = self.client.get(f"/api/environments/@current/") + response_data = response.json() + + self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) + self.assertEqual(self.not_found_response(), response_data) + + def test_fetch_nonexistent_team(self): + response = self.client.get(f"/api/environments/234444/") + response_data = response.json() + + self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) + self.assertEqual(self.not_found_response(), response_data) + + def test_list_teams_restricted_ones_hidden(self): + self.organization_membership.level = OrganizationMembership.Level.MEMBER + self.organization_membership.save() + Team.objects.create( + organization=self.organization, + name="Other", + access_control=True, + ) + + # The other team should not be returned as it's restricted for the logged-in user + projects_response = self.client.get(f"/api/environments/") + + # 9 (above): + with self.assertNumQueries(FuzzyInt(9, 10)): + current_org_response = self.client.get(f"/api/organizations/{self.organization.id}/") + + self.assertEqual(projects_response.status_code, HTTP_200_OK) + self.assertEqual( + projects_response.json().get("results"), + [ + { + "id": self.team.id, + "uuid": str(self.team.uuid), + "organization": str(self.organization.id), + "api_token": self.team.api_token, + "name": self.team.name, + "completed_snippet_onboarding": False, + "has_completed_onboarding_for": {"product_analytics": True}, + "ingested_event": False, + "is_demo": False, + "timezone": "UTC", + "access_control": False, + } + ], + ) + self.assertEqual(current_org_response.status_code, HTTP_200_OK) + self.assertEqual( + current_org_response.json().get("teams"), + [ + { + "id": self.team.id, + "uuid": str(self.team.uuid), + "organization": str(self.organization.id), + "api_token": self.team.api_token, + "name": self.team.name, + "completed_snippet_onboarding": False, + "has_completed_onboarding_for": {"product_analytics": True}, + "ingested_event": False, + "is_demo": False, + "timezone": "UTC", + "access_control": False, + } + ], + ) + + return TestTeamEnterpriseAPI + + +class TestTeamEnterpriseAPI(team_enterprise_api_test_factory()): + pass diff --git a/ee/clickhouse/views/experiments.py b/ee/clickhouse/views/experiments.py index ffdbfcc16e428..7aed519d29ee6 100644 --- a/ee/clickhouse/views/experiments.py +++ b/ee/clickhouse/views/experiments.py @@ -333,7 +333,7 @@ def update(self, instance: Experiment, validated_data: dict, *args: Any, **kwarg return super().update(instance, validated_data) -class ClickhouseExperimentsViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): +class EnterpriseExperimentsViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): scope_object = "experiment" serializer_class = ExperimentSerializer queryset = Experiment.objects.prefetch_related("feature_flag", "created_by").all() diff --git a/ee/clickhouse/views/groups.py b/ee/clickhouse/views/groups.py index 3c20275b2de7b..bfbb375e70990 100644 --- a/ee/clickhouse/views/groups.py +++ b/ee/clickhouse/views/groups.py @@ -25,7 +25,7 @@ class Meta: read_only_fields = ["group_type", "group_type_index"] -class ClickhouseGroupsTypesView(TeamAndOrgViewSetMixin, mixins.ListModelMixin, viewsets.GenericViewSet): +class GroupsTypesViewSet(TeamAndOrgViewSetMixin, mixins.ListModelMixin, viewsets.GenericViewSet): scope_object = "group" serializer_class = GroupTypeSerializer queryset = GroupTypeMapping.objects.all().order_by("group_type_index") @@ -54,7 +54,7 @@ class Meta: fields = ["group_type_index", "group_key", "group_properties", "created_at"] -class ClickhouseGroupsView(TeamAndOrgViewSetMixin, mixins.ListModelMixin, viewsets.GenericViewSet): +class GroupsViewSet(TeamAndOrgViewSetMixin, mixins.ListModelMixin, viewsets.GenericViewSet): scope_object = "group" serializer_class = GroupSerializer queryset = Group.objects.all() diff --git a/ee/clickhouse/views/insights.py b/ee/clickhouse/views/insights.py index 6072ab2957bb1..933928428ab50 100644 --- a/ee/clickhouse/views/insights.py +++ b/ee/clickhouse/views/insights.py @@ -26,7 +26,7 @@ def has_object_permission(self, request: Request, view, insight: Insight) -> boo return view.user_permissions.insight(insight).effective_privilege_level == Dashboard.PrivilegeLevel.CAN_EDIT -class ClickhouseInsightsViewSet(InsightViewSet): +class EnterpriseInsightsViewSet(InsightViewSet): permission_classes = [CanEditInsight] retention_query_class = ClickhouseRetention stickiness_query_class = ClickhouseStickiness diff --git a/ee/urls.py b/ee/urls.py index 68ede5f20126c..633766add1439 100644 --- a/ee/urls.py +++ b/ee/urls.py @@ -4,10 +4,8 @@ from django.contrib import admin from django.urls import include from django.urls.conf import path -from rest_framework_extensions.routers import NestedRegistryItem from ee.api import integration -from posthog.api.routing import DefaultRouterPlusPlus from .api import ( authentication, @@ -25,14 +23,16 @@ from .session_recordings import session_recording_playlist -def extend_api_router( - root_router: DefaultRouterPlusPlus, - *, - projects_router: NestedRegistryItem, - organizations_router: NestedRegistryItem, - project_dashboards_router: NestedRegistryItem, - project_feature_flags_router: NestedRegistryItem, -) -> None: +def extend_api_router() -> None: + from posthog.api import ( + router as root_router, + register_grandfathered_environment_nested_viewset, + projects_router, + organizations_router, + project_feature_flags_router, + project_dashboards_router, + ) + root_router.register(r"billing", billing.BillingViewset, "billing") root_router.register(r"license", license.LicenseViewSet) root_router.register(r"integrations", integration.PublicIntegrationViewSet) @@ -60,8 +60,8 @@ def extend_api_router( "organization_resource_access", ["organization_id"], ) - projects_router.register(r"hooks", hooks.HookViewSet, "environment_hooks", ["team_id"]) - projects_router.register( + register_grandfathered_environment_nested_viewset(r"hooks", hooks.HookViewSet, "environment_hooks", ["team_id"]) + register_grandfathered_environment_nested_viewset( r"explicit_members", explicit_team_member.ExplicitTeamMemberViewSet, "environment_explicit_members", @@ -74,7 +74,7 @@ def extend_api_router( ["project_id", "dashboard_id"], ) - projects_router.register( + register_grandfathered_environment_nested_viewset( r"subscriptions", subscription.SubscriptionViewSet, "environment_subscriptions", ["team_id"] ) projects_router.register( diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--dark.png index 667f5c839eea3bd665c9ad98bcb1a1bdd156c0de..24cfa930cc9e105726073d25bddc5d0d6f1f6d2d 100644 GIT binary patch literal 154427 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[pJ?tAL|e~#h~L*!IvTPk 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 diff --git a/frontend/src/lib/api.mock.ts b/frontend/src/lib/api.mock.ts index c6ca0a28c2cda..e10d348595cdd 100644 --- a/frontend/src/lib/api.mock.ts +++ b/frontend/src/lib/api.mock.ts @@ -12,6 +12,7 @@ import { PluginConfigWithPluginInfo, PluginInstallationType, PluginType, + ProjectType, PropertyFilterType, PropertyOperator, TeamType, @@ -90,6 +91,13 @@ export const MOCK_DEFAULT_TEAM: TeamType = { live_events_token: '123', } +export const MOCK_DEFAULT_PROJECT: ProjectType = { + id: MOCK_TEAM_ID, + name: 'MockHog App + Marketing', + organization_id: MOCK_ORGANIZATION_ID, + created_at: '2020-06-30T09:53:35.932534Z', +} + export const MOCK_DEFAULT_ORGANIZATION: OrganizationType = { customer_id: null, id: MOCK_ORGANIZATION_ID, diff --git a/frontend/src/types.ts b/frontend/src/types.ts index f5c24f30f0508..1000b66e54f06 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -469,6 +469,12 @@ export interface SessionRecordingAIConfig { important_user_properties: string[] } +export interface ProjectType { + id: number + name: string + organization_id: string + created_at: string +} export interface TeamType extends TeamBasicType { created_at: string updated_at: string @@ -3273,6 +3279,7 @@ export type EventOrPropType = EventDefinition & PropertyDefinition export interface AppContext { current_user: UserType | null + current_project: ProjectType | null current_team: TeamType | TeamPublicType | null preflight: PreflightStatus default_event_name: string diff --git a/posthog/api/__init__.py b/posthog/api/__init__.py index c6fd6851da9c7..3419e09b50209 100644 --- a/posthog/api/__init__.py +++ b/posthog/api/__init__.py @@ -1,5 +1,8 @@ -from rest_framework import decorators, exceptions +from rest_framework import decorators, exceptions, viewsets +from rest_framework_extensions.routers import NestedRegistryItem + +from posthog.api import project from posthog.api.routing import DefaultRouterPlusPlus from posthog.batch_exports import http as batch_exports from posthog.settings import EE_AVAILABLE @@ -81,35 +84,57 @@ def api_not_found(request): router.register(r"feature_flag", feature_flag.LegacyFeatureFlagViewSet) # Used for library side feature flag evaluation # Nested endpoints shared -projects_router = router.register(r"projects", team.RootTeamViewSet, "projects") -project_plugins_configs_router = projects_router.register( +projects_router = router.register(r"projects", project.RootProjectViewSet, "projects") +environments_router = router.register(r"environments", team.RootTeamViewSet, "environments") + + +def register_grandfathered_environment_nested_viewset( + prefix: str, viewset: type[viewsets.GenericViewSet], basename: str, parents_query_lookups: list[str] +) -> tuple[NestedRegistryItem, NestedRegistryItem]: + """ + Register the environment-specific viewset under both /environments/:team_id/ (correct endpoint) + and /projects/:team_id/ (legacy, but supported for backward compatibility endpoint). + DO NOT USE ON ANY NEW ENDPOINT YOU'RE ADDING! + """ + if parents_query_lookups[0] != "team_id": + raise ValueError("Only endpoints with team_id as the first parent query lookup can be environment-nested") + if not basename.startswith("environment_"): + raise ValueError("Only endpoints with a basename starting with `environment_` can be environment-nested") + environment_nested = environments_router.register(prefix, viewset, basename, parents_query_lookups) + legacy_project_nested = projects_router.register( + prefix, viewset, basename.replace("environment_", "project_"), parents_query_lookups + ) + return environment_nested, legacy_project_nested + + +register_grandfathered_environment_nested_viewset( r"plugin_configs", plugin.PluginConfigViewSet, "environment_plugin_configs", ["team_id"] ) -project_plugins_configs_router.register( +register_grandfathered_environment_nested_viewset( r"logs", plugin_log_entry.PluginLogEntryViewSet, "environment_plugin_config_logs", ["team_id", "plugin_config_id"], ) -projects_router.register( +register_grandfathered_environment_nested_viewset( r"pipeline_transformation_configs", plugin.PipelineTransformationsConfigsViewSet, "environment_pipeline_transformation_configs", ["team_id"], ) -projects_router.register( +register_grandfathered_environment_nested_viewset( r"pipeline_destination_configs", plugin.PipelineDestinationsConfigsViewSet, "environment_pipeline_destination_configs", ["team_id"], ) -projects_router.register( +register_grandfathered_environment_nested_viewset( r"pipeline_frontend_apps_configs", plugin.PipelineFrontendAppsConfigsViewSet, "environment_pipeline_frontend_apps_configs", ["team_id"], ) -projects_router.register( +register_grandfathered_environment_nested_viewset( r"pipeline_import_apps_configs", plugin.PipelineImportAppsConfigsViewSet, "environment_pipeline_import_apps_configs", @@ -147,9 +172,13 @@ def api_not_found(request): r"dashboards", dashboard.DashboardsViewSet, "project_dashboards", ["project_id"] ) -projects_router.register(r"exports", exports.ExportedAssetViewSet, "environment_exports", ["team_id"]) -projects_router.register(r"integrations", integration.IntegrationViewSet, "environment_integrations", ["team_id"]) -projects_router.register( +register_grandfathered_environment_nested_viewset( + r"exports", exports.ExportedAssetViewSet, "environment_exports", ["team_id"] +) +register_grandfathered_environment_nested_viewset( + r"integrations", integration.IntegrationViewSet, "environment_integrations", ["team_id"] +) +register_grandfathered_environment_nested_viewset( r"ingestion_warnings", ingestion_warnings.IngestionWarningsViewSet, "environment_ingestion_warnings", @@ -170,37 +199,50 @@ def api_not_found(request): ["project_id"], ) -app_metrics_router = projects_router.register( +environment_app_metrics_router, legacy_project_app_metrics_router = register_grandfathered_environment_nested_viewset( r"app_metrics", app_metrics.AppMetricsViewSet, "environment_app_metrics", ["team_id"] ) -app_metrics_router.register( +environment_app_metrics_router.register( r"historical_exports", app_metrics.HistoricalExportsAppMetricsViewSet, "environment_app_metrics_historical_exports", ["team_id", "plugin_config_id"], ) +legacy_project_app_metrics_router.register( + r"historical_exports", + app_metrics.HistoricalExportsAppMetricsViewSet, + "project_app_metrics_historical_exports", + ["team_id", "plugin_config_id"], +) -batch_exports_router = projects_router.register( - r"batch_exports", batch_exports.BatchExportViewSet, "environment_batch_exports", ["team_id"] +environment_batch_exports_router, legacy_project_batch_exports_router = ( + register_grandfathered_environment_nested_viewset( + r"batch_exports", batch_exports.BatchExportViewSet, "environment_batch_exports", ["team_id"] + ) ) -batch_export_runs_router = batch_exports_router.register( +environment_batch_exports_router.register( r"runs", batch_exports.BatchExportRunViewSet, "environment_batch_export_runs", ["team_id", "batch_export_id"] ) +legacy_project_batch_exports_router.register( + r"runs", batch_exports.BatchExportRunViewSet, "project_batch_export_runs", ["team_id", "batch_export_id"] +) -projects_router.register(r"warehouse_tables", table.TableViewSet, "environment_warehouse_tables", ["team_id"]) -projects_router.register( +register_grandfathered_environment_nested_viewset( + r"warehouse_tables", table.TableViewSet, "environment_warehouse_tables", ["team_id"] +) +register_grandfathered_environment_nested_viewset( r"warehouse_saved_queries", saved_query.DataWarehouseSavedQueryViewSet, "environment_warehouse_saved_queries", ["team_id"], ) -projects_router.register( +register_grandfathered_environment_nested_viewset( r"warehouse_view_links", view_link.ViewLinkViewSet, "environment_warehouse_view_links", ["team_id"], ) -projects_router.register( +register_grandfathered_environment_nested_viewset( r"warehouse_view_link", view_link.ViewLinkViewSet, "environment_warehouse_view_link", ["team_id"] ) @@ -220,10 +262,10 @@ def api_not_found(request): projects_router.register(r"uploaded_media", uploaded_media.MediaViewSet, "project_media", ["project_id"]) projects_router.register(r"tags", tagged_item.TaggedItemViewSet, "project_tags", ["project_id"]) -projects_router.register(r"query", query.QueryViewSet, "environment_query", ["team_id"]) +register_grandfathered_environment_nested_viewset(r"query", query.QueryViewSet, "environment_query", ["team_id"]) # External data resources -projects_router.register( +register_grandfathered_environment_nested_viewset( r"external_data_sources", external_data_source.ExternalDataSourceViewSet, "environment_external_data_sources", @@ -243,16 +285,16 @@ def api_not_found(request): ) -projects_router.register( +register_grandfathered_environment_nested_viewset( r"external_data_schemas", external_data_schema.ExternalDataSchemaViewset, - "project_external_data_schemas", + "environment_external_data_schemas", ["team_id"], ) # Organizations nested endpoints organizations_router = router.register(r"organizations", organization.OrganizationViewSet, "organizations") -organizations_router.register(r"projects", team.TeamViewSet, "projects", ["organization_id"]) +organizations_router.register(r"projects", project.ProjectViewSet, "organization_projects", ["organization_id"]) organizations_router.register( r"batch_exports", batch_exports.BatchExportOrganizationViewSet, "batch_exports", ["organization_id"] ) @@ -316,10 +358,10 @@ def api_not_found(request): # General endpoints (shared across CH & PG) router.register(r"login", authentication.LoginViewSet, "login") -router.register(r"login/token", authentication.TwoFactorViewSet) -router.register(r"login/precheck", authentication.LoginPrecheckViewSet) +router.register(r"login/token", authentication.TwoFactorViewSet, "login_token") +router.register(r"login/precheck", authentication.LoginPrecheckViewSet, "login_precheck") router.register(r"reset", authentication.PasswordResetViewSet, "password_reset") -router.register(r"users", user.UserViewSet) +router.register(r"users", user.UserViewSet, "users") router.register(r"personal_api_keys", personal_api_key.PersonalAPIKeyViewSet, "personal_api_keys") router.register(r"instance_status", instance_status.InstanceStatusViewSet, "instance_status") router.register(r"dead_letter_queue", dead_letter_queue.DeadLetterQueueViewSet, "dead_letter_queue") @@ -344,44 +386,46 @@ def api_not_found(request): router.register(r"event", LegacyEventViewSet, basename="event") # Nested endpoints CH -projects_router.register(r"events", EventViewSet, "environment_events", ["team_id"]) +register_grandfathered_environment_nested_viewset(r"events", EventViewSet, "environment_events", ["team_id"]) projects_router.register(r"actions", ActionViewSet, "project_actions", ["project_id"]) projects_router.register(r"cohorts", CohortViewSet, "project_cohorts", ["project_id"]) -projects_router.register(r"persons", PersonViewSet, "environment_persons", ["team_id"]) -projects_router.register(r"elements", ElementViewSet, "environment_elements", ["team_id"]) # TODO: Can be removed? -project_session_recordings_router = projects_router.register( - r"session_recordings", - SessionRecordingViewSet, - "environment_session_recordings", - ["team_id"], +register_grandfathered_environment_nested_viewset( + r"elements", + ElementViewSet, + "environment_elements", + ["team_id"], # TODO: Can be removed? +) +environment_sessions_recordings_router, legacy_project_session_recordings_router = ( + register_grandfathered_environment_nested_viewset( + r"session_recordings", + SessionRecordingViewSet, + "environment_session_recordings", + ["team_id"], + ) ) -projects_router.register(r"heatmaps", HeatmapViewSet, "environment_heatmaps", ["team_id"]) -projects_router.register(r"sessions", SessionViewSet, "environment_sessions", ["team_id"]) +register_grandfathered_environment_nested_viewset(r"heatmaps", HeatmapViewSet, "environment_heatmaps", ["team_id"]) +register_grandfathered_environment_nested_viewset(r"sessions", SessionViewSet, "environment_sessions", ["team_id"]) if EE_AVAILABLE: - from ee.clickhouse.views.experiments import ClickhouseExperimentsViewSet - from ee.clickhouse.views.groups import ( - ClickhouseGroupsTypesView, - ClickhouseGroupsView, - ) - from ee.clickhouse.views.insights import ClickhouseInsightsViewSet - from ee.clickhouse.views.person import ( - EnterprisePersonViewSet, - LegacyEnterprisePersonViewSet, - ) - - projects_router.register(r"experiments", ClickhouseExperimentsViewSet, "project_experiments", ["project_id"]) - projects_router.register(r"groups", ClickhouseGroupsView, "environment_groups", ["team_id"]) - projects_router.register(r"groups_types", ClickhouseGroupsTypesView, "project_groups_types", ["project_id"]) + from ee.clickhouse.views.experiments import EnterpriseExperimentsViewSet + from ee.clickhouse.views.groups import GroupsTypesViewSet, GroupsViewSet + from ee.clickhouse.views.insights import EnterpriseInsightsViewSet + from ee.clickhouse.views.person import EnterprisePersonViewSet, LegacyEnterprisePersonViewSet + + projects_router.register(r"experiments", EnterpriseExperimentsViewSet, "project_experiments", ["project_id"]) + register_grandfathered_environment_nested_viewset(r"groups", GroupsViewSet, "environment_groups", ["team_id"]) + projects_router.register(r"groups_types", GroupsTypesViewSet, "project_groups_types", ["project_id"]) project_insights_router = projects_router.register( - r"insights", ClickhouseInsightsViewSet, "project_insights", ["project_id"] + r"insights", EnterpriseInsightsViewSet, "project_insights", ["project_id"] + ) + register_grandfathered_environment_nested_viewset( + r"persons", EnterprisePersonViewSet, "environment_persons", ["team_id"] ) - projects_router.register(r"persons", EnterprisePersonViewSet, "environment_persons", ["team_id"]) - router.register(r"person", LegacyEnterprisePersonViewSet, basename="person") + router.register(r"person", LegacyEnterprisePersonViewSet, "persons") else: project_insights_router = projects_router.register(r"insights", InsightViewSet, "project_insights", ["project_id"]) - projects_router.register(r"persons", PersonViewSet, "environment_persons", ["team_id"]) - router.register(r"person", LegacyPersonViewSet, basename="person") + register_grandfathered_environment_nested_viewset(r"persons", PersonViewSet, "environment_persons", ["team_id"]) + router.register(r"person", LegacyPersonViewSet, "persons") project_dashboards_router.register( @@ -412,12 +456,18 @@ def api_not_found(request): ["team_id", "insight_id"], ) -project_session_recordings_router.register( +environment_sessions_recordings_router.register( r"sharing", sharing.SharingConfigurationViewSet, "environment_recording_sharing", ["team_id", "recording_id"], ) +legacy_project_session_recordings_router.register( + r"sharing", + sharing.SharingConfigurationViewSet, + "project_recording_sharing", + ["team_id", "recording_id"], +) projects_router.register( r"notebooks", @@ -440,7 +490,7 @@ def api_not_found(request): ["project_id"], ) -projects_router.register( +register_grandfathered_environment_nested_viewset( r"hog_functions", hog_function.HogFunctionViewSet, "environment_hog_functions", @@ -454,7 +504,7 @@ def api_not_found(request): ["project_id"], ) -projects_router.register( +register_grandfathered_environment_nested_viewset( r"alerts", alert.AlertViewSet, "environment_alerts", diff --git a/posthog/api/project.py b/posthog/api/project.py new file mode 100644 index 0000000000000..d2e3e341228b0 --- /dev/null +++ b/posthog/api/project.py @@ -0,0 +1,508 @@ +from datetime import timedelta +from functools import cached_property +from typing import Any, Optional, cast + +from django.shortcuts import get_object_or_404 +from loginas.utils import is_impersonated_session +from rest_framework import exceptions, request, response, serializers, viewsets +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated + +from posthog.api.geoip import get_geoip_properties +from posthog.api.routing import TeamAndOrgViewSetMixin +from posthog.api.shared import ProjectBasicSerializer +from posthog.api.team import PremiumMultiProjectPermissions, TeamSerializer, validate_team_attrs +from posthog.event_usage import report_user_action +from posthog.jwt import PosthogJwtAudience, encode_jwt +from posthog.models import User +from posthog.models.activity_logging.activity_log import ( + Detail, + dict_changes_between, + load_activity, + log_activity, +) +from posthog.models.activity_logging.activity_page import activity_page_response +from posthog.models.async_deletion import AsyncDeletion, DeletionType +from posthog.models.group_type_mapping import GroupTypeMapping +from posthog.models.organization import OrganizationMembership +from posthog.models.personal_api_key import APIScopeObjectOrNotSupported +from posthog.models.project import Project +from posthog.models.signals import mute_selected_signals +from posthog.models.team.team import Team +from posthog.models.team.util import delete_batch_exports, delete_bulky_postgres_data +from posthog.models.utils import UUIDT +from posthog.permissions import ( + APIScopePermission, + OrganizationAdminWritePermissions, + OrganizationMemberPermissions, + TeamMemberLightManagementPermission, + TeamMemberStrictManagementPermission, +) +from posthog.user_permissions import UserPermissions, UserPermissionsSerializerMixin +from posthog.utils import get_ip_address, get_week_start_for_country_code + + +class ProjectSerializer(ProjectBasicSerializer, UserPermissionsSerializerMixin): + effective_membership_level = serializers.SerializerMethodField() # Compat with TeamSerializer + has_group_types = serializers.SerializerMethodField() # Compat with TeamSerializer + live_events_token = serializers.SerializerMethodField() # Compat with TeamSerializer + + class Meta: + model = Project + fields = ( + "id", + "organization", + "name", + "created_at", + "effective_membership_level", # Compat with TeamSerializer + "has_group_types", # Compat with TeamSerializer + "live_events_token", # Compat with TeamSerializer + "updated_at", + "uuid", # Compat with TeamSerializer + "api_token", # Compat with TeamSerializer + "app_urls", # Compat with TeamSerializer + "slack_incoming_webhook", # Compat with TeamSerializer + "anonymize_ips", # Compat with TeamSerializer + "completed_snippet_onboarding", # Compat with TeamSerializer + "ingested_event", # Compat with TeamSerializer + "test_account_filters", # Compat with TeamSerializer + "test_account_filters_default_checked", # Compat with TeamSerializer + "path_cleaning_filters", # Compat with TeamSerializer + "is_demo", # Compat with TeamSerializer + "timezone", # Compat with TeamSerializer + "data_attributes", # Compat with TeamSerializer + "person_display_name_properties", # Compat with TeamSerializer + "correlation_config", # Compat with TeamSerializer + "autocapture_opt_out", # Compat with TeamSerializer + "autocapture_exceptions_opt_in", # Compat with TeamSerializer + "autocapture_web_vitals_opt_in", # Compat with TeamSerializer + "autocapture_web_vitals_allowed_metrics", # Compat with TeamSerializer + "autocapture_exceptions_errors_to_ignore", # Compat with TeamSerializer + "capture_console_log_opt_in", # Compat with TeamSerializer + "capture_performance_opt_in", # Compat with TeamSerializer + "session_recording_opt_in", # Compat with TeamSerializer + "session_recording_sample_rate", # Compat with TeamSerializer + "session_recording_minimum_duration_milliseconds", # Compat with TeamSerializer + "session_recording_linked_flag", # Compat with TeamSerializer + "session_recording_network_payload_capture_config", # Compat with TeamSerializer + "session_replay_config", # Compat with TeamSerializer + "access_control", # Compat with TeamSerializer + "week_start_day", # Compat with TeamSerializer + "primary_dashboard", # Compat with TeamSerializer + "live_events_columns", # Compat with TeamSerializer + "recording_domains", # Compat with TeamSerializer + "person_on_events_querying_enabled", # Compat with TeamSerializer + "inject_web_apps", # Compat with TeamSerializer + "extra_settings", # Compat with TeamSerializer + "modifiers", # Compat with TeamSerializer + "default_modifiers", # Compat with TeamSerializer + "has_completed_onboarding_for", # Compat with TeamSerializer + "surveys_opt_in", # Compat with TeamSerializer + "heatmaps_opt_in", # Compat with TeamSerializer + ) + read_only_fields = ( + "id", + "uuid", + "organization", + "effective_membership_level", + "has_group_types", + "live_events_token", + "created_at", + "api_token", + "updated_at", + "ingested_event", + "default_modifiers", + "person_on_events_querying_enabled", + ) + + team_passthrough_fields = { + "updated_at", + "uuid", + "api_token", + "app_urls", + "slack_incoming_webhook", + "anonymize_ips", + "completed_snippet_onboarding", + "ingested_event", + "test_account_filters", + "test_account_filters_default_checked", + "path_cleaning_filters", + "is_demo", + "timezone", + "data_attributes", + "person_display_name_properties", + "correlation_config", + "autocapture_opt_out", + "autocapture_exceptions_opt_in", + "autocapture_web_vitals_opt_in", + "autocapture_web_vitals_allowed_metrics", + "autocapture_exceptions_errors_to_ignore", + "capture_console_log_opt_in", + "capture_performance_opt_in", + "session_recording_opt_in", + "session_recording_sample_rate", + "session_recording_minimum_duration_milliseconds", + "session_recording_linked_flag", + "session_recording_network_payload_capture_config", + "session_replay_config", + "access_control", + "week_start_day", + "primary_dashboard", + "live_events_columns", + "recording_domains", + "person_on_events_querying_enabled", + "inject_web_apps", + "extra_settings", + "modifiers", + "default_modifiers", + "has_completed_onboarding_for", + "surveys_opt_in", + "heatmaps_opt_in", + } + + def get_effective_membership_level(self, project: Project) -> Optional[OrganizationMembership.Level]: + team = project.teams.get(pk=project.pk) + return self.user_permissions.team(team).effective_membership_level + + def get_has_group_types(self, project: Project) -> bool: + return GroupTypeMapping.objects.filter(team_id=project.id).exists() + + def get_live_events_token(self, project: Project) -> Optional[str]: + team = project.teams.get(pk=project.pk) + return encode_jwt( + {"team_id": team.id, "api_token": team.api_token}, + timedelta(days=7), + PosthogJwtAudience.LIVESTREAM, + ) + + @staticmethod + def validate_session_recording_linked_flag(value) -> dict | None: + return TeamSerializer.validate_session_recording_linked_flag(value) + + @staticmethod + def validate_session_recording_network_payload_capture_config(value) -> dict | None: + return TeamSerializer.validate_session_recording_network_payload_capture_config(value) + + @staticmethod + def validate_session_replay_config(value) -> dict | None: + return TeamSerializer.validate_session_replay_config(value) + + @staticmethod + def validate_session_replay_ai_summary_config(value: dict | None) -> dict | None: + return TeamSerializer.validate_session_replay_ai_summary_config(value) + + def validate(self, attrs: Any) -> Any: + attrs = validate_team_attrs(attrs, self.context["view"], self.context["request"], self.instance) + return super().validate(attrs) + + def create(self, validated_data: dict[str, Any], **kwargs) -> Project: + serializers.raise_errors_on_nested_writes("create", self, validated_data) + request = self.context["request"] + + if "week_start_day" not in validated_data: + country_code = get_geoip_properties(get_ip_address(request)).get("$geoip_country_code", None) + if country_code: + week_start_day_for_user_ip_location = get_week_start_for_country_code(country_code) + # get_week_start_for_country_code() also returns 6 for countries where the week starts on Saturday, + # but ClickHouse doesn't support Saturday as the first day of the week, so we fall back to Sunday + validated_data["week_start_day"] = 1 if week_start_day_for_user_ip_location == 1 else 0 + + team_fields: dict[str, Any] = {} + for field_name in validated_data.copy(): # Copy to avoid iterating over a changing dict + if field_name in self.Meta.team_passthrough_fields: + team_fields[field_name] = validated_data.pop(field_name) + project, team = Project.objects.create_with_team( + organization_id=self.context["view"].organization_id, + initiating_user=self.context["request"].user, + **validated_data, + team_fields=team_fields, + ) + + request.user.current_team = team + request.user.team = request.user.current_team # Update cached property + request.user.save() + + log_activity( + organization_id=project.organization_id, + team_id=project.pk, + user=request.user, + was_impersonated=is_impersonated_session(request), + scope="Project", + item_id=project.pk, + activity="created", + detail=Detail(name=str(project.name)), + ) + log_activity( + organization_id=project.organization_id, + team_id=team.pk, + user=request.user, + was_impersonated=is_impersonated_session(request), + scope="Team", + item_id=team.pk, + activity="created", + detail=Detail(name=str(team.name)), + ) + + return project + + def update(self, instance: Project, validated_data: dict[str, Any]) -> Project: + team = instance.passthrough_team + team_before_update = team.__dict__.copy() + project_before_update = instance.__dict__.copy() + + if ( + "session_replay_config" in validated_data + and validated_data["session_replay_config"] is not None + and team.session_replay_config is not None + ): + # for session_replay_config and its top level keys we merge existing settings with new settings + # this way we don't always have to receive the entire settings object to change one setting + # so for each key in validated_data["session_replay_config"] we merge it with the existing settings + # and then merge any top level keys that weren't provided + + for key, value in validated_data["session_replay_config"].items(): + if key in team.session_replay_config: + # if they're both dicts then we merge them, otherwise, the new value overwrites the old + if isinstance(team.session_replay_config[key], dict) and isinstance( + validated_data["session_replay_config"][key], dict + ): + validated_data["session_replay_config"][key] = { + **team.session_replay_config[key], # existing values + **value, # and new values on top + } + + # then also add back in any keys that exist but are not in the provided data + validated_data["session_replay_config"] = { + **team.session_replay_config, + **validated_data["session_replay_config"], + } + + should_team_be_saved_too = False + for attr, value in validated_data.items(): + if attr in self.Meta.team_passthrough_fields: + should_team_be_saved_too = True + setattr(team, attr, value) + else: + if attr == "name": # `name` should be updated on _both_ the Project and Team + should_team_be_saved_too = True + setattr(team, attr, value) + setattr(instance, attr, value) + + instance.save() + if should_team_be_saved_too: + team.save() + + team_after_update = team.__dict__.copy() + project_after_update = instance.__dict__.copy() + team_changes = dict_changes_between("Team", team_before_update, team_after_update, use_field_exclusions=True) + project_changes = dict_changes_between( + "Project", project_before_update, project_after_update, use_field_exclusions=True + ) + + if team_changes: + log_activity( + organization_id=cast(UUIDT, instance.organization_id), + team_id=instance.pk, + user=cast(User, self.context["request"].user), + was_impersonated=is_impersonated_session(request), + scope="Team", + item_id=instance.pk, + activity="updated", + detail=Detail( + name=str(team.name), + changes=team_changes, + ), + ) + if project_changes: + log_activity( + organization_id=cast(UUIDT, instance.organization_id), + team_id=instance.pk, + user=cast(User, self.context["request"].user), + was_impersonated=is_impersonated_session(request), + scope="Project", + item_id=instance.pk, + activity="updated", + detail=Detail( + name=str(instance.name), + changes=project_changes, + ), + ) + + return instance + + +class ProjectViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): + """ + Projects for the current organization. + """ + + scope_object: APIScopeObjectOrNotSupported = "project" + serializer_class = ProjectSerializer + queryset = Project.objects.all().select_related("organization").prefetch_related("teams") + lookup_field = "id" + ordering = "-created_by" + + def safely_get_queryset(self, queryset): + # IMPORTANT: This is actually what ensures that a user cannot read/update a project for which they don't have permission + visible_teams_ids = UserPermissions(cast(User, self.request.user)).team_ids_visible_for_user + return queryset.filter(id__in=visible_teams_ids) + + def get_serializer_class(self) -> type[serializers.BaseSerializer]: + if self.action == "list": + return ProjectBasicSerializer + return super().get_serializer_class() + + # NOTE: Team permissions are somewhat complex so we override the underlying viewset's get_permissions method + def dangerously_get_permissions(self) -> list: + """ + Special permissions handling for create requests as the organization is inferred from the current user. + """ + + permissions: list = [ + IsAuthenticated, + APIScopePermission, + PremiumMultiProjectPermissions, + *self.permission_classes, + ] + + # Return early for non-actions (e.g. OPTIONS) + if self.action: + if self.action == "create": + if "is_demo" not in self.request.data or not self.request.data["is_demo"]: + permissions.append(OrganizationAdminWritePermissions) + else: + permissions.append(OrganizationMemberPermissions) + elif self.action != "list": + # Skip TeamMemberAccessPermission for list action, as list is serialized with limited TeamBasicSerializer + permissions.append(TeamMemberLightManagementPermission) + + return [permission() for permission in permissions] + + def safely_get_object(self, queryset): + lookup_value = self.kwargs[self.lookup_field] + if lookup_value == "@current": + team = getattr(self.request.user, "team", None) + if team is None: + raise exceptions.NotFound() + return team.project + + filter_kwargs = {self.lookup_field: lookup_value} + try: + project = get_object_or_404(queryset, **filter_kwargs) + except ValueError as error: + raise exceptions.ValidationError(str(error)) + return project + + # :KLUDGE: Exposed for compatibility reasons for permission classes. + @property + def team(self): + project = self.get_object() + return project.teams.get(id=project.id) + + def perform_destroy(self, project: Project): + project_id = project.pk + organization_id = project.organization_id + project_name = project.name + + user = cast(User, self.request.user) + + teams: list[Team] = list(project.teams.all()) + delete_bulky_postgres_data(team_ids=[team.id for team in teams]) + delete_batch_exports(team_ids=[team.pk for team in teams]) + + with mute_selected_signals(): + super().perform_destroy(project) + + # Once the project is deleted, queue deletion of associated data + AsyncDeletion.objects.bulk_create( + [ + AsyncDeletion( + deletion_type=DeletionType.Team, + team_id=team.id, + key=str(team.id), + created_by=user, + ) + for team in teams + ], + ignore_conflicts=True, + ) + + for team in teams: + log_activity( + organization_id=cast(UUIDT, organization_id), + team_id=team.pk, + user=user, + was_impersonated=is_impersonated_session(self.request), + scope="Team", + item_id=team.pk, + activity="deleted", + detail=Detail(name=str(team.name)), + ) + report_user_action(user, f"team deleted", team=team) + log_activity( + organization_id=cast(UUIDT, organization_id), + team_id=project_id, + user=user, + was_impersonated=is_impersonated_session(self.request), + scope="Project", + item_id=project_id, + activity="deleted", + detail=Detail(name=str(project_name)), + ) + report_user_action( + user, + f"project deleted", + {"project_name": project_name}, + team=teams[0], + ) + + @action( + methods=["PATCH"], + detail=True, + # Only ADMIN or higher users are allowed to access this project + permission_classes=[TeamMemberStrictManagementPermission], + ) + def reset_token(self, request: request.Request, id: str, **kwargs) -> response.Response: + project = self.get_object() + project.passthrough_team.reset_token_and_save( + user=request.user, is_impersonated_session=is_impersonated_session(request) + ) + return response.Response(ProjectSerializer(project, context=self.get_serializer_context()).data) + + @action( + methods=["GET"], + detail=True, + permission_classes=[IsAuthenticated], + ) + def is_generating_demo_data(self, request: request.Request, id: str, **kwargs) -> response.Response: + project = self.get_object() + return response.Response({"is_generating_demo_data": project.passthrough_team.get_is_generating_demo_data()}) + + @action(methods=["GET"], detail=True) + def activity(self, request: request.Request, **kwargs): + # TODO: This is currently the same as in TeamViewSet - we should rework for the Project scope + limit = int(request.query_params.get("limit", "10")) + page = int(request.query_params.get("page", "1")) + + project = self.get_object() + + activity_page = load_activity( + scope="Team", + team_id=project.pk, + item_ids=[str(project.pk)], + limit=limit, + page=page, + ) + return activity_page_response(activity_page, limit, page, request) + + @cached_property + def user_permissions(self): + project = self.get_object() if self.action == "reset_token" else None + team = project.passthrough_team if project else None + return UserPermissions(cast(User, self.request.user), team) + + +class RootProjectViewSet(ProjectViewSet): + # NOTE: We don't want people creating projects via the "current_organization" concept, but rather specify the org ID + # in the URL - hence this is hidden from the API docs, but used in the app + hide_api_docs = True diff --git a/posthog/api/routing.py b/posthog/api/routing.py index d2f902e30b5b4..0f1784b3ac4a2 100644 --- a/posthog/api/routing.py +++ b/posthog/api/routing.py @@ -38,32 +38,6 @@ class DefaultRouterPlusPlus(ExtendedDefaultRouter): """DefaultRouter with optional trailing slash and drf-extensions nesting.""" - # This is an override because of changes in djangorestframework 3.15, which is required for python 3.11 - # changes taken from and explained here: https://github.com/nautobot/nautobot/pull/5546/files#diff-81850a2ccad5814aab4f477d447f85cc0a82e9c10fd88fd72327cda51a750471R30 - def _register(self, prefix, viewset, basename=None): - """ - Override DRF's BaseRouter.register() to bypass an unnecessary restriction added in version 3.15.0. - (Reference: https://github.com/encode/django-rest-framework/pull/8438) - """ - if basename is None: - basename = self.get_default_basename(viewset) - - # DRF: - # if self.is_already_registered(basename): - # msg = (f'Router with basename "{basename}" is already registered. ' - # f'Please provide a unique basename for viewset "{viewset}"') - # raise ImproperlyConfigured(msg) - # - # We bypass this because we have at least one use case (/api/extras/jobs/) where we are *intentionally* - # registering two viewsets with the same basename, but have carefully defined them so as not to conflict. - - # resuming standard DRF code... - self.registry.append((prefix, viewset, basename)) - - # invalidate the urls cache - if hasattr(self, "_urls"): - del self._urls - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.trailing_slash = r"/?" @@ -72,7 +46,7 @@ def __init__(self, *args, **kwargs): # NOTE: Previously known as the StructuredViewSetMixin # IMPORTANT: Almost all viewsets should inherit from this mixin. It should be the first thing it inherits from to ensure # that typing works as expected -class TeamAndOrgViewSetMixin(_GenericViewSet): +class TeamAndOrgViewSetMixin(_GenericViewSet): # TODO: Rename to include "Env" in name # This flag disables nested routing handling, reverting to the old request.user.team behavior # Allows for a smoother transition from the old flat API structure to the newer nested one param_derived_from_user_current_team: Optional[Literal["team_id", "project_id"]] = None diff --git a/posthog/api/shared.py b/posthog/api/shared.py index e37fe9de29297..fc849eb2e2b42 100644 --- a/posthog/api/shared.py +++ b/posthog/api/shared.py @@ -2,12 +2,17 @@ This module contains serializers that are used across other serializers for nested representations. """ -from typing import Optional +import copy +from typing import Any, Optional from rest_framework import serializers from posthog.models import Organization, Team, User from posthog.models.organization import OrganizationMembership +from posthog.models.project import Project +from rest_framework.fields import SkipField +from rest_framework.relations import PKOnlyObject +from rest_framework.utils import model_meta class UserBasicSerializer(serializers.ModelSerializer): @@ -36,6 +41,112 @@ def get_hedgehog_config(self, user: User) -> Optional[dict]: return None +class ProjectBasicSerializer(serializers.ModelSerializer): + """ + Serializer for `Project` model with minimal attributes to speeed up loading and transfer times. + Also used for nested serializers. + """ + + class Meta: + model = Project + fields = ( + "id", + "uuid", # Compat with TeamSerializer + "organization", + "api_token", # Compat with TeamSerializer + "name", + "completed_snippet_onboarding", # Compat with TeamSerializer + "has_completed_onboarding_for", # Compat with TeamSerializer + "ingested_event", # Compat with TeamSerializer + "is_demo", # Compat with TeamSerializer + "timezone", # Compat with TeamSerializer + "access_control", # Compat with TeamSerializer + ) + read_only_fields = fields + team_passthrough_fields = { + "uuid", + "api_token", + "completed_snippet_onboarding", + "has_completed_onboarding_for", + "ingested_event", + "is_demo", + "timezone", + "access_control", + } + + def get_fields(self): + declared_fields = copy.deepcopy(self._declared_fields) + + info = model_meta.get_field_info(Project) + team_info = model_meta.get_field_info(Team) + for field_name, field in team_info.fields.items(): + if field_name in info.fields: + continue + info.fields[field_name] = field + info.fields_and_pk[field_name] = field + for field_name, relation in team_info.forward_relations.items(): + if field_name in info.forward_relations: + continue + info.forward_relations[field_name] = relation + info.relations[field_name] = relation + for accessor_name, relation in team_info.reverse_relations.items(): + if accessor_name in info.reverse_relations: + continue + info.reverse_relations[accessor_name] = relation + info.relations[accessor_name] = relation + + field_names = self.get_field_names(declared_fields, info) + + extra_kwargs = self.get_extra_kwargs() + extra_kwargs, hidden_fields = self.get_uniqueness_extra_kwargs(field_names, declared_fields, extra_kwargs) + + fields = {} + for field_name in field_names: + if field_name in declared_fields: + fields[field_name] = declared_fields[field_name] + continue + extra_field_kwargs = extra_kwargs.get(field_name, {}) + source = extra_field_kwargs.get("source", "*") + if source == "*": + source = field_name + field_class, field_kwargs = self.build_field(source, info, model_class=Project, nested_depth=0) + field_kwargs = self.include_extra_kwargs(field_kwargs, extra_field_kwargs) + fields[field_name] = field_class(**field_kwargs) + fields.update(hidden_fields) + return fields + + def build_field(self, field_name, info, model_class, nested_depth): + if field_name in self.Meta.team_passthrough_fields: + model_class = Team + return super().build_field(field_name, info, model_class, nested_depth) + + def to_representation(self, instance): + """ + Object instance -> Dict of primitive datatypes. Basically copied from Serializer.to_representation + """ + ret: dict[str, Any] = {} + fields = self._readable_fields + + for field in fields: + assert field.field_name is not None + try: + attribute_source = instance + if field.field_name in self.Meta.team_passthrough_fields: + # This branch is the only material change from the original method + attribute_source = instance.passthrough_team + attribute = field.get_attribute(attribute_source) + except SkipField: + continue + + check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute + if check_for_none is None: + ret[field.field_name] = None + else: + ret[field.field_name] = field.to_representation(attribute) + + return ret + + class TeamBasicSerializer(serializers.ModelSerializer): """ Serializer for `Team` model with minimal attributes to speeed up loading and transfer times. diff --git a/posthog/api/signup.py b/posthog/api/signup.py index 7cda79d66195d..3847999ec551d 100644 --- a/posthog/api/signup.py +++ b/posthog/api/signup.py @@ -161,7 +161,7 @@ def enter_demo(self, validated_data) -> User: return self._user def create_team(self, organization: Organization, user: User) -> Team: - return Team.objects.create_with_data(user=user, organization=organization) + return Team.objects.create_with_data(initiating_user=user, organization=organization) def to_representation(self, instance) -> dict: data = UserBasicSerializer(instance=instance).data diff --git a/posthog/api/team.py b/posthog/api/team.py index 00584574186ec..34349958a6e88 100644 --- a/posthog/api/team.py +++ b/posthog/api/team.py @@ -3,22 +3,20 @@ from typing import Any, Optional, cast from datetime import timedelta -from django.core.cache import cache from django.shortcuts import get_object_or_404 from loginas.utils import is_impersonated_session from posthog.jwt import PosthogJwtAudience, encode_jwt from rest_framework.permissions import BasePermission, IsAuthenticated -from rest_framework import exceptions, request, response, serializers, viewsets from posthog.api.utils import action +from rest_framework import exceptions, request, response, serializers, viewsets from posthog.api.geoip import get_geoip_properties from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import TeamBasicSerializer from posthog.constants import AvailableFeature from posthog.event_usage import report_user_action -from posthog.models import InsightCachingState, Team, User +from posthog.models import Team, User from posthog.models.activity_logging.activity_log import ( - Change, Detail, dict_changes_between, load_activity, @@ -30,9 +28,8 @@ from posthog.models.organization import OrganizationMembership from posthog.models.personal_api_key import APIScopeObjectOrNotSupported from posthog.models.signals import mute_selected_signals -from posthog.models.team.team import set_team_in_cache from posthog.models.team.util import delete_batch_exports, delete_bulky_postgres_data -from posthog.models.utils import UUIDT, generate_random_token_project +from posthog.models.utils import UUIDT from posthog.permissions import ( CREATE_METHODS, APIScopePermission, @@ -42,15 +39,14 @@ TeamMemberStrictManagementPermission, get_organization_from_view, ) -from posthog.tasks.demo_create_data import create_data_for_demo_team from posthog.user_permissions import UserPermissions, UserPermissionsSerializerMixin from posthog.utils import get_ip_address, get_week_start_for_country_code -class PremiumMultiProjectPermissions(BasePermission): +class PremiumMultiProjectPermissions(BasePermission): # TODO: Rename to include "Env" in name """Require user to have all necessary premium features on their plan for create access to the endpoint.""" - message = "You must upgrade your PostHog plan to be able to create and manage multiple projects." + message = "You must upgrade your PostHog plan to be able to create and manage multiple projects or environments." def has_permission(self, request: request.Request, view) -> bool: if request.method in CREATE_METHODS: @@ -190,7 +186,7 @@ def get_effective_membership_level(self, team: Team) -> Optional[OrganizationMem return self.user_permissions.team(team).effective_membership_level def get_has_group_types(self, team: Team) -> bool: - return GroupTypeMapping.objects.filter(team=team).exists() + return GroupTypeMapping.objects.filter(team_id=team.id).exists() def get_live_events_token(self, team: Team) -> Optional[str]: return encode_jwt( @@ -199,7 +195,8 @@ def get_live_events_token(self, team: Team) -> Optional[str]: PosthogJwtAudience.LIVESTREAM, ) - def validate_session_recording_linked_flag(self, value) -> dict | None: + @staticmethod + def validate_session_recording_linked_flag(value) -> dict | None: if value is None: return None @@ -217,7 +214,8 @@ def validate_session_recording_linked_flag(self, value) -> dict | None: return value - def validate_session_recording_network_payload_capture_config(self, value) -> dict | None: + @staticmethod + def validate_session_recording_network_payload_capture_config(value) -> dict | None: if value is None: return None @@ -231,7 +229,8 @@ def validate_session_recording_network_payload_capture_config(self, value) -> di return value - def validate_session_replay_config(self, value) -> dict | None: + @staticmethod + def validate_session_replay_config(value) -> dict | None: if value is None: return None @@ -245,11 +244,12 @@ def validate_session_replay_config(self, value) -> dict | None: ) if "ai_config" in value: - self.validate_session_replay_ai_summary_config(value["ai_config"]) + TeamSerializer.validate_session_replay_ai_summary_config(value["ai_config"]) return value - def validate_session_replay_ai_summary_config(self, value: dict | None) -> dict | None: + @staticmethod + def validate_session_replay_ai_summary_config(value: dict | None) -> dict | None: if value is not None: if not isinstance(value, dict): raise exceptions.ValidationError("Must provide a dictionary or None.") @@ -269,44 +269,12 @@ def validate_session_replay_ai_summary_config(self, value: dict | None) -> dict return value def validate(self, attrs: Any) -> Any: - if "primary_dashboard" in attrs and attrs["primary_dashboard"].team != self.instance: - raise exceptions.PermissionDenied("Dashboard does not belong to this team.") - - if "access_control" in attrs: - # Only organization-wide admins and above should be allowed to switch the project between open and private - # If a project-only admin who is only an org member disabled this it, they wouldn't be able to reenable it - request = self.context["request"] - if isinstance(self.instance, Team): - organization_id = self.instance.organization_id - else: - organization_id = self.context["view"].organization - org_membership: OrganizationMembership = OrganizationMembership.objects.only("level").get( - organization_id=organization_id, user=request.user - ) - if org_membership.level < OrganizationMembership.Level.ADMIN: - raise exceptions.PermissionDenied("Your organization access level is insufficient.") - - if "autocapture_exceptions_errors_to_ignore" in attrs: - if not isinstance(attrs["autocapture_exceptions_errors_to_ignore"], list): - raise exceptions.ValidationError( - "Must provide a list for field: autocapture_exceptions_errors_to_ignore." - ) - for error in attrs["autocapture_exceptions_errors_to_ignore"]: - if not isinstance(error, str): - raise exceptions.ValidationError( - "Must provide a list of strings to field: autocapture_exceptions_errors_to_ignore." - ) - - if len(json.dumps(attrs["autocapture_exceptions_errors_to_ignore"])) > 300: - raise exceptions.ValidationError( - "Field autocapture_exceptions_errors_to_ignore must be less than 300 characters. Complex config should be provided in posthog-js initialization." - ) + attrs = validate_team_attrs(attrs, self.context["view"], self.context["request"], self.instance) return super().validate(attrs) def create(self, validated_data: dict[str, Any], **kwargs) -> Team: serializers.raise_errors_on_nested_writes("create", self, validated_data) request = self.context["request"] - organization = self.context["view"].organization # Use the org we used to validate permissions if "week_start_day" not in validated_data: country_code = get_geoip_properties(get_ip_address(request)).get("$geoip_country_code", None) @@ -316,20 +284,18 @@ def create(self, validated_data: dict[str, Any], **kwargs) -> Team: # but ClickHouse doesn't support Saturday as the first day of the week, so we fall back to Sunday validated_data["week_start_day"] = 1 if week_start_day_for_user_ip_location == 1 else 0 - if validated_data.get("is_demo", False): - team = Team.objects.create(**validated_data, organization=organization) - cache_key = f"is_generating_demo_data_{team.pk}" - cache.set(cache_key, "True") # create an item in the cache that we can use to see if the demo data is ready - create_data_for_demo_team.delay(team.pk, request.user.pk, cache_key) - else: - team = Team.objects.create_with_data(**validated_data, organization=organization) + team = Team.objects.create_with_data( + initiating_user=self.context["request"].user, + organization=self.context["view"].organization, + **validated_data, + ) request.user.current_team = team request.user.team = request.user.current_team # Update cached property request.user.save() log_activity( - organization_id=organization.id, + organization_id=team.organization_id, team_id=team.pk, user=request.user, was_impersonated=is_impersonated_session(request), @@ -341,22 +307,9 @@ def create(self, validated_data: dict[str, Any], **kwargs) -> Team: return team - def _clear_team_insight_caching_states(self, team: Team) -> None: - # TODO: Remove this method: - # 1. It only clear the cache for saved insights, queries not linked to one are being ignored here - # 2. We should anyway 100% be relying on cache keys being different for materially different queries, instead of - # on remembering to call this method when project settings change. We probably already are in the clear here! - hashes = InsightCachingState.objects.filter(team=team).values_list("cache_key", flat=True) - cache.delete_many(hashes) - def update(self, instance: Team, validated_data: dict[str, Any]) -> Team: before_update = instance.__dict__.copy() - if ("timezone" in validated_data and validated_data["timezone"] != instance.timezone) or ( - "modifiers" in validated_data and validated_data["modifiers"] != instance.modifiers - ): - self._clear_team_insight_caching_states(instance) - if ( "session_replay_config" in validated_data and validated_data["session_replay_config"] is not None @@ -409,7 +362,7 @@ class TeamViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): Projects for the current organization. """ - scope_object: APIScopeObjectOrNotSupported = "project" + scope_object: APIScopeObjectOrNotSupported = "project" # TODO: Change to `environment` on environments rollout serializer_class = TeamSerializer queryset = Team.objects.all().select_related("organization") lookup_field = "id" @@ -507,7 +460,7 @@ def perform_destroy(self, team: Team): activity="deleted", detail=Detail(name=str(team_name)), ) - # TRICKY: We pass in Team here as otherwise the access to "current_team" can fail if it was deleted + # TRICKY: We pass in `team` here as access to `user.current_team` can fail if it was deleted report_user_action(user, f"team deleted", team=team) @action( @@ -518,31 +471,7 @@ def perform_destroy(self, team: Team): ) def reset_token(self, request: request.Request, id: str, **kwargs) -> response.Response: team = self.get_object() - old_token = team.api_token - team.api_token = generate_random_token_project() - team.save() - - log_activity( - organization_id=team.organization_id, - team_id=team.pk, - user=cast(User, request.user), - was_impersonated=is_impersonated_session(request), - scope="Team", - item_id=team.pk, - activity="updated", - detail=Detail( - name=str(team.name), - changes=[ - Change( - type="Team", - action="changed", - field="api_token", - ) - ], - ), - ) - - set_team_in_cache(old_token, None) + team.reset_token_and_save(user=request.user, is_impersonated_session=is_impersonated_session(request)) return response.Response(TeamSerializer(team, context=self.get_serializer_context()).data) @action( @@ -552,8 +481,7 @@ def reset_token(self, request: request.Request, id: str, **kwargs) -> response.R ) def is_generating_demo_data(self, request: request.Request, id: str, **kwargs) -> response.Response: team = self.get_object() - cache_key = f"is_generating_demo_data_{team.pk}" - return response.Response({"is_generating_demo_data": cache.get(cache_key) == "True"}) + return response.Response({"is_generating_demo_data": team.get_is_generating_demo_data()}) @action(methods=["GET"], detail=True) def activity(self, request: request.Request, **kwargs): @@ -578,7 +506,38 @@ def user_permissions(self): class RootTeamViewSet(TeamViewSet): - # NOTE: We don't want people managing projects via the "current_organization" concept. - # Rather specifying the org ID at the top level - we still support it for backwards compat but don't document it anymore. - + # NOTE: We don't want people creating environments via the "current_organization"/"current_project" concept, but + # rather specify the org ID and project ID in the URL - hence this is hidden from the API docs, but used in the app hide_api_docs = True + + +def validate_team_attrs( + attrs: dict[str, Any], view: TeamAndOrgViewSetMixin, request: request.Request, instance +) -> dict[str, Any]: + if "primary_dashboard" in attrs and attrs["primary_dashboard"].team_id != instance.id: + raise exceptions.PermissionDenied("Dashboard does not belong to this team.") + + if "access_control" in attrs: + assert isinstance(request.user, User) + # Only organization-wide admins and above should be allowed to switch the project between open and private + # If a project-only admin who is only an org member disabled this it, they wouldn't be able to reenable it + org_membership: OrganizationMembership = OrganizationMembership.objects.only("level").get( + organization_id=instance.organization_id, user=request.user + ) + if org_membership.level < OrganizationMembership.Level.ADMIN: + raise exceptions.PermissionDenied("Your organization access level is insufficient.") + + if "autocapture_exceptions_errors_to_ignore" in attrs: + if not isinstance(attrs["autocapture_exceptions_errors_to_ignore"], list): + raise exceptions.ValidationError("Must provide a list for field: autocapture_exceptions_errors_to_ignore.") + for error in attrs["autocapture_exceptions_errors_to_ignore"]: + if not isinstance(error, str): + raise exceptions.ValidationError( + "Must provide a list of strings to field: autocapture_exceptions_errors_to_ignore." + ) + + if len(json.dumps(attrs["autocapture_exceptions_errors_to_ignore"])) > 300: + raise exceptions.ValidationError( + "Field autocapture_exceptions_errors_to_ignore must be less than 300 characters. Complex config should be provided in posthog-js initialization." + ) + return attrs diff --git a/posthog/api/test/__snapshots__/test_api_docs.ambr b/posthog/api/test/__snapshots__/test_api_docs.ambr index 9b470bb936f43..fb629e787c4f6 100644 --- a/posthog/api/test/__snapshots__/test_api_docs.ambr +++ b/posthog/api/test/__snapshots__/test_api_docs.ambr @@ -1,19 +1,7 @@ # serializer version: 1 # name: TestAPIDocsSchema.test_api_docs_generation_warnings_snapshot list([ - '/home/runner/work/posthog/posthog/posthog/api/organization.py: Warning [OrganizationViewSet > OrganizationSerializer]: unable to resolve type hint for function "get_metadata". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/organization.py: Warning [OrganizationViewSet > OrganizationSerializer]: unable to resolve type hint for function "get_member_count". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/batch_exports/http.py: Warning [BatchExportOrganizationViewSet]: could not derive type of path parameter "organization_id" because model "posthog.batch_exports.models.BatchExport" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/posthog/batch_exports/http.py: Warning [BatchExportOrganizationViewSet > BatchExportSerializer]: could not resolve serializer field "HogQLSelectQueryField(required=False)". Defaulting to "string"', - '/home/runner/work/posthog/posthog/posthog/api/plugin.py: Warning [PipelineDestinationsViewSet > PluginSerializer]: unable to resolve type hint for function "get_hog_function_migration_available". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/proxy_record.py: Warning [ProxyRecordViewset]: could not derive type of path parameter "organization_id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/posthog/api/proxy_record.py: Warning [ProxyRecordViewset]: could not derive type of path parameter "id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/ee/api/role.py: Warning [RoleViewSet > RoleSerializer]: unable to resolve type hint for function "get_members". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/ee/api/role.py: Warning [RoleViewSet > RoleSerializer]: unable to resolve type hint for function "get_associated_flags". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/ee/api/role.py: Warning [RoleMembershipViewSet]: could not derive type of path parameter "organization_id" because model "ee.models.role.RoleMembership" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/posthog/api/action.py: Warning [ActionViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.action.action.Action" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/posthog/api/activity_log.py: Warning [ActivityLogViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.activity_logging.activity_log.ActivityLog" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/posthog/api/annotation.py: Warning [AnnotationsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.annotation.Annotation" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/posthog/api/plugin_log_entry.py: Warning [PluginLogEntryViewSet]: could not derive type of path parameter "plugin_config_id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', "/home/runner/work/posthog/posthog/posthog/api/app_metrics.py: Error [AppMetricsViewSet]: exception raised while getting serializer. Hint: Is get_serializer_class() returning None or is get_queryset() not working without a request? Ignoring the view for now. (Exception: 'AppMetricsViewSet' should either include a `serializer_class` attribute, or override the `get_serializer_class()` method.)", '/home/runner/work/posthog/posthog/posthog/api/app_metrics.py: Warning [AppMetricsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.plugin.PluginConfig" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/app_metrics.py: Error [HistoricalExportsAppMetricsViewSet]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.', @@ -21,15 +9,8 @@ '/home/runner/work/posthog/posthog/posthog/api/app_metrics.py: Warning [HistoricalExportsAppMetricsViewSet]: could not derive type of path parameter "plugin_config_id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/app_metrics.py: Warning [HistoricalExportsAppMetricsViewSet]: could not derive type of path parameter "id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/batch_exports/http.py: Warning [BatchExportViewSet]: could not derive type of path parameter "project_id" because model "posthog.batch_exports.models.BatchExport" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/posthog/batch_exports/http.py: Warning [BatchExportViewSet > BatchExportSerializer]: could not resolve serializer field "HogQLSelectQueryField(required=False)". Defaulting to "string"', '/home/runner/work/posthog/posthog/posthog/batch_exports/http.py: Warning [BatchExportRunViewSet]: could not derive type of path parameter "project_id" because model "posthog.batch_exports.models.BatchExportRun" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/posthog/api/cohort.py: Warning [CohortViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.cohort.cohort.Cohort" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/posthog/api/dashboards/dashboard_templates.py: Warning [DashboardTemplateViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.dashboard_templates.DashboardTemplate" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/posthog/api/dashboards/dashboard.py: Warning [DashboardsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.dashboard.Dashboard" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/ee/api/dashboard_collaborator.py: Warning [DashboardCollaboratorViewSet]: could not derive type of path parameter "project_id" because model "ee.models.dashboard_privilege.DashboardPrivilege" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/posthog/api/sharing.py: Warning [SharingConfigurationViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.sharing_configuration.SharingConfiguration" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/posthog/api/early_access_feature.py: Warning [EarlyAccessFeatureViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.early_access_feature.EarlyAccessFeature" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - "/home/runner/work/posthog/posthog/posthog/api/event_definition.py: Error [EventDefinitionViewSet]: exception raised while getting serializer. Hint: Is get_serializer_class() returning None or is get_queryset() not working without a request? Ignoring the view for now. (Exception: 'AnonymousUser' object has no attribute 'organization')", - '/home/runner/work/posthog/posthog/posthog/api/event_definition.py: Warning [EventDefinitionViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.event_definition.EventDefinition" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/event.py: Warning [EventViewSet]: could not derive type of path parameter "project_id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/models/event/util.py: Warning [EventViewSet > ClickhouseEventSerializer]: unable to resolve type hint for function "get_id". Consider using a type hint or @extend_schema_field. Defaulting to string.', '/home/runner/work/posthog/posthog/posthog/models/event/util.py: Warning [EventViewSet > ClickhouseEventSerializer]: unable to resolve type hint for function "get_distinct_id". Consider using a type hint or @extend_schema_field. Defaulting to string.', @@ -40,29 +21,11 @@ '/home/runner/work/posthog/posthog/posthog/models/event/util.py: Warning [EventViewSet > ClickhouseEventSerializer]: unable to resolve type hint for function "get_elements". Consider using a type hint or @extend_schema_field. Defaulting to string.', '/home/runner/work/posthog/posthog/posthog/models/event/util.py: Warning [EventViewSet > ClickhouseEventSerializer]: unable to resolve type hint for function "get_elements_chain". Consider using a type hint or @extend_schema_field. Defaulting to string.', '/home/runner/work/posthog/posthog/posthog/api/event.py: Warning [EventViewSet]: could not derive type of path parameter "id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/ee/clickhouse/views/experiments.py: Warning [ClickhouseExperimentsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.experiment.Experiment" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/ee/api/explicit_team_member.py: Warning [ExplicitTeamMemberViewSet]: could not derive type of path parameter "project_id" because model "ee.models.explicit_team_membership.ExplicitTeamMembership" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/exports.py: Warning [ExportedAssetViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.exported_asset.ExportedAsset" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/exports.py: Warning [ExportedAssetViewSet > ExportedAssetSerializer]: unable to resolve type hint for function "has_content". Consider using a type hint or @extend_schema_field. Defaulting to string.', '/home/runner/work/posthog/posthog/posthog/api/exports.py: Warning [ExportedAssetViewSet > ExportedAssetSerializer]: unable to resolve type hint for function "filename". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/feature_flag.py: Warning [FeatureFlagViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.feature_flag.feature_flag.FeatureFlag" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/ee/api/feature_flag_role_access.py: Warning [FeatureFlagRoleAccessViewSet]: could not derive type of path parameter "project_id" because model "ee.models.feature_flag_role_access.FeatureFlagRoleAccess" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/ee/clickhouse/views/groups.py: Warning [ClickhouseGroupsView]: could not derive type of path parameter "project_id" because model "posthog.models.group.group.Group" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/ee/clickhouse/views/groups.py: Warning [ClickhouseGroupsTypesView]: could not derive type of path parameter "project_id" because model "posthog.models.group_type_mapping.GroupTypeMapping" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/ee/clickhouse/views/insights.py: Warning [ClickhouseInsightsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.insight.Insight" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [ClickhouseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_last_refresh". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [ClickhouseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_cache_target_age". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [ClickhouseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_next_allowed_client_refresh". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [ClickhouseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_result". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [ClickhouseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_hasMore". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [ClickhouseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_columns". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [ClickhouseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_timezone". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [ClickhouseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_is_cached". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [ClickhouseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_query_status". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [ClickhouseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_hogql". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [ClickhouseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_types". Consider using a type hint or @extend_schema_field. Defaulting to string.', - '/home/runner/work/posthog/posthog/posthog/api/notebook.py: Warning [NotebookViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.notebook.notebook.Notebook" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/posthog/api/person.py: Warning [PersonViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.person.person.Person" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/ee/clickhouse/views/groups.py: Warning [GroupsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.group.group.Group" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/ee/clickhouse/views/person.py: Warning [EnterprisePersonViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.person.person.Person" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/plugin.py: Warning [PipelineDestinationsConfigsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.plugin.PluginConfig" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/plugin.py: Warning [PipelineDestinationsConfigsViewSet > PluginConfigSerializer]: unable to resolve type hint for function "get_config". Consider using a type hint or @extend_schema_field. Defaulting to string.', @@ -73,28 +36,67 @@ '/home/runner/work/posthog/posthog/posthog/api/plugin.py: Warning [PipelineImportAppsConfigsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.plugin.PluginConfig" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/plugin.py: Warning [PipelineTransformationsConfigsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.plugin.PluginConfig" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/plugin.py: Warning [PluginConfigViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.plugin.PluginConfig" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/posthog/api/plugin_log_entry.py: Warning [PluginLogEntryViewSet]: could not derive type of path parameter "project_id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/posthog/api/plugin_log_entry.py: Warning [PluginLogEntryViewSet]: could not derive type of path parameter "plugin_config_id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', - "/home/runner/work/posthog/posthog/posthog/api/property_definition.py: Error [PropertyDefinitionViewSet]: exception raised while getting serializer. Hint: Is get_serializer_class() returning None or is get_queryset() not working without a request? Ignoring the view for now. (Exception: 'AnonymousUser' object has no attribute 'organization')", - '/home/runner/work/posthog/posthog/posthog/api/property_definition.py: Warning [PropertyDefinitionViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.property_definition.PropertyDefinition" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/query.py: Warning [QueryViewSet]: could not derive type of path parameter "project_id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', '/opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/pydantic/_internal/_model_construction.py: Warning [QueryViewSet > ModelMetaclass]: Encountered 2 components with identical names "Person" and different classes and . This will very likely result in an incorrect schema. Try renaming one.', '/home/runner/work/posthog/posthog/posthog/api/query.py: Warning [QueryViewSet]: could not derive type of path parameter "id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/query.py: Error [QueryViewSet]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.', - '/home/runner/work/posthog/posthog/ee/session_recordings/session_recording_playlist.py: Warning [SessionRecordingPlaylistViewSet]: could not derive type of path parameter "project_id" because model "posthog.session_recordings.models.session_recording_playlist.SessionRecordingPlaylist" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/ee/session_recordings/session_recording_playlist.py: Warning [SessionRecordingPlaylistViewSet]: could not derive type of path parameter "session_recording_id" because model "posthog.session_recordings.models.session_recording_playlist.SessionRecordingPlaylist" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/session_recordings/session_recording_api.py: Warning [SessionRecordingViewSet]: could not derive type of path parameter "project_id" because model "posthog.session_recordings.models.session_recording.SessionRecording" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/session_recordings/session_recording_api.py: Warning [SessionRecordingViewSet > SessionRecordingSerializer]: could not resolve field on model with path "viewed". This is likely a custom field that does some unknown magic. Maybe consider annotating the field/property? Defaulting to "string". (Exception: SessionRecording has no field named \'viewed\')', '/home/runner/work/posthog/posthog/posthog/api/person.py: Warning [SessionRecordingViewSet > SessionRecordingSerializer > MinimalPersonSerializer]: unable to resolve type hint for function "get_distinct_ids". Consider using a type hint or @extend_schema_field. Defaulting to string.', '/home/runner/work/posthog/posthog/posthog/session_recordings/session_recording_api.py: Warning [SessionRecordingViewSet > SessionRecordingSerializer]: unable to resolve type hint for function "storage". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/sharing.py: Warning [SharingConfigurationViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.sharing_configuration.SharingConfiguration" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/session.py: Warning [SessionViewSet]: could not derive type of path parameter "project_id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/ee/api/subscription.py: Warning [SubscriptionViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.subscription.Subscription" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/ee/api/subscription.py: Warning [SubscriptionViewSet > SubscriptionSerializer]: unable to resolve type hint for function "summary". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/organization.py: Warning [OrganizationViewSet > OrganizationSerializer]: unable to resolve type hint for function "get_metadata". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/organization.py: Warning [OrganizationViewSet > OrganizationSerializer]: unable to resolve type hint for function "get_member_count". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/batch_exports/http.py: Warning [BatchExportOrganizationViewSet]: could not derive type of path parameter "organization_id" because model "posthog.batch_exports.models.BatchExport" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/posthog/api/plugin.py: Warning [PipelineDestinationsViewSet > PluginSerializer]: unable to resolve type hint for function "get_hog_function_migration_available". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/project.py: Warning [ProjectViewSet > ProjectSerializer]: could not resolve field on model with path "person_on_events_querying_enabled". This is likely a custom field that does some unknown magic. Maybe consider annotating the field/property? Defaulting to "string". (Exception: Project has no field named \'person_on_events_querying_enabled\')', + '/home/runner/work/posthog/posthog/posthog/api/project.py: Warning [ProjectViewSet > ProjectSerializer]: could not resolve field on model with path "default_modifiers". This is likely a custom field that does some unknown magic. Maybe consider annotating the field/property? Defaulting to "string". (Exception: Project has no field named \'default_modifiers\')', + '/home/runner/work/posthog/posthog/posthog/api/proxy_record.py: Warning [ProxyRecordViewset]: could not derive type of path parameter "organization_id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/posthog/api/proxy_record.py: Warning [ProxyRecordViewset]: could not derive type of path parameter "id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/ee/api/role.py: Warning [RoleViewSet > RoleSerializer]: unable to resolve type hint for function "get_members". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/ee/api/role.py: Warning [RoleViewSet > RoleSerializer]: unable to resolve type hint for function "get_associated_flags". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/ee/api/role.py: Warning [RoleMembershipViewSet]: could not derive type of path parameter "organization_id" because model "ee.models.role.RoleMembership" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/posthog/api/action.py: Warning [ActionViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.action.action.Action" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/posthog/api/activity_log.py: Warning [ActivityLogViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.activity_logging.activity_log.ActivityLog" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/posthog/api/annotation.py: Warning [AnnotationsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.annotation.Annotation" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/posthog/api/cohort.py: Warning [CohortViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.cohort.cohort.Cohort" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/posthog/api/dashboards/dashboard_templates.py: Warning [DashboardTemplateViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.dashboard_templates.DashboardTemplate" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/posthog/api/dashboards/dashboard.py: Warning [DashboardsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.dashboard.Dashboard" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/ee/api/dashboard_collaborator.py: Warning [DashboardCollaboratorViewSet]: could not derive type of path parameter "project_id" because model "ee.models.dashboard_privilege.DashboardPrivilege" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/posthog/api/early_access_feature.py: Warning [EarlyAccessFeatureViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.early_access_feature.EarlyAccessFeature" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + "/home/runner/work/posthog/posthog/posthog/api/event_definition.py: Error [EventDefinitionViewSet]: exception raised while getting serializer. Hint: Is get_serializer_class() returning None or is get_queryset() not working without a request? Ignoring the view for now. (Exception: 'AnonymousUser' object has no attribute 'organization')", + '/home/runner/work/posthog/posthog/posthog/api/event_definition.py: Warning [EventDefinitionViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.event_definition.EventDefinition" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/ee/clickhouse/views/experiments.py: Warning [EnterpriseExperimentsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.experiment.Experiment" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/posthog/api/feature_flag.py: Warning [FeatureFlagViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.feature_flag.feature_flag.FeatureFlag" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/ee/api/feature_flag_role_access.py: Warning [FeatureFlagRoleAccessViewSet]: could not derive type of path parameter "project_id" because model "ee.models.feature_flag_role_access.FeatureFlagRoleAccess" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/ee/clickhouse/views/groups.py: Warning [GroupsTypesViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.group_type_mapping.GroupTypeMapping" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/ee/clickhouse/views/insights.py: Warning [EnterpriseInsightsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.insight.Insight" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_last_refresh". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_cache_target_age". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_next_allowed_client_refresh". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_result". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_hasMore". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_columns". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_timezone". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_is_cached". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_query_status". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_hogql". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/insight.py: Warning [EnterpriseInsightsViewSet > InsightSerializer]: unable to resolve type hint for function "get_types". Consider using a type hint or @extend_schema_field. Defaulting to string.', + '/home/runner/work/posthog/posthog/posthog/api/notebook.py: Warning [NotebookViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.notebook.notebook.Notebook" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + "/home/runner/work/posthog/posthog/posthog/api/property_definition.py: Error [PropertyDefinitionViewSet]: exception raised while getting serializer. Hint: Is get_serializer_class() returning None or is get_queryset() not working without a request? Ignoring the view for now. (Exception: 'AnonymousUser' object has no attribute 'organization')", + '/home/runner/work/posthog/posthog/posthog/api/property_definition.py: Warning [PropertyDefinitionViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.property_definition.PropertyDefinition" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/ee/session_recordings/session_recording_playlist.py: Warning [SessionRecordingPlaylistViewSet]: could not derive type of path parameter "project_id" because model "posthog.session_recordings.models.session_recording_playlist.SessionRecordingPlaylist" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/ee/session_recordings/session_recording_playlist.py: Warning [SessionRecordingPlaylistViewSet]: could not derive type of path parameter "session_recording_id" because model "posthog.session_recordings.models.session_recording_playlist.SessionRecordingPlaylist" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/survey.py: Warning [SurveyViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.feedback.survey.Survey" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/survey.py: Warning [SurveyViewSet > SurveySerializer]: unable to resolve type hint for function "get_conditions". Consider using a type hint or @extend_schema_field. Defaulting to string.', 'Warning: encountered multiple names for the same choice set (HrefMatchingEnum). This may be unwanted even though the generated schema is technically correct. Add an entry to ENUM_NAME_OVERRIDES to fix the naming.', 'Warning: encountered multiple names for the same choice set (EffectivePrivilegeLevelEnum). This may be unwanted even though the generated schema is technically correct. Add an entry to ENUM_NAME_OVERRIDES to fix the naming.', 'Warning: encountered multiple names for the same choice set (MembershipLevelEnum). This may be unwanted even though the generated schema is technically correct. Add an entry to ENUM_NAME_OVERRIDES to fix the naming.', + 'Warning: operationId "environments_app_metrics_historical_exports_retrieve" has collisions [(\'/api/environments/{project_id}/app_metrics/{plugin_config_id}/historical_exports/\', \'get\'), (\'/api/environments/{project_id}/app_metrics/{plugin_config_id}/historical_exports/{id}/\', \'get\')]. resolving with numeral suffixes.', + 'Warning: operationId "environments_persons_activity_retrieve" has collisions [(\'/api/environments/{project_id}/persons/{id}/activity/\', \'get\'), (\'/api/environments/{project_id}/persons/activity/\', \'get\')]. resolving with numeral suffixes.', 'Warning: operationId "list" has collisions [(\'/api/organizations/\', \'get\'), (\'/api/organizations/{organization_id}/projects/\', \'get\')]. resolving with numeral suffixes.', 'Warning: operationId "create" has collisions [(\'/api/organizations/\', \'post\'), (\'/api/organizations/{organization_id}/projects/\', \'post\')]. resolving with numeral suffixes.', 'Warning: operationId "retrieve" has collisions [(\'/api/organizations/{id}/\', \'get\'), (\'/api/organizations/{organization_id}/projects/{id}/\', \'get\')]. resolving with numeral suffixes.', diff --git a/posthog/api/test/__snapshots__/test_decide.ambr b/posthog/api/test/__snapshots__/test_decide.ambr index d375d41ab6314..2d1f760a286fb 100644 --- a/posthog/api/test/__snapshots__/test_decide.ambr +++ b/posthog/api/test/__snapshots__/test_decide.ambr @@ -64,6 +64,244 @@ ''' # --- # name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.10 + ''' + SELECT "posthog_hogfunction"."id", + "posthog_hogfunction"."team_id", + "posthog_hogfunction"."name", + "posthog_hogfunction"."description", + "posthog_hogfunction"."created_at", + "posthog_hogfunction"."created_by_id", + "posthog_hogfunction"."deleted", + "posthog_hogfunction"."updated_at", + "posthog_hogfunction"."enabled", + "posthog_hogfunction"."icon_url", + "posthog_hogfunction"."hog", + "posthog_hogfunction"."bytecode", + "posthog_hogfunction"."inputs_schema", + "posthog_hogfunction"."inputs", + "posthog_hogfunction"."filters", + "posthog_hogfunction"."masking", + "posthog_hogfunction"."template_id", + "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."project_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_web_vitals_opt_in", + "posthog_team"."autocapture_web_vitals_allowed_metrics", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."session_replay_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."heatmaps_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."modifiers", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."plugins_opt_in", + "posthog_team"."opt_out_capture", + "posthog_team"."event_names", + "posthog_team"."event_names_with_usage", + "posthog_team"."event_properties", + "posthog_team"."event_properties_with_usage", + "posthog_team"."event_properties_numerical", + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" + FROM "posthog_hogfunction" + INNER JOIN "posthog_team" ON ("posthog_hogfunction"."team_id" = "posthog_team"."id") + WHERE ("posthog_hogfunction"."team_id" = 2 + AND "posthog_hogfunction"."filters" @> '{"filter_test_accounts": true}'::jsonb) + ''' +# --- +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.11 + ''' + SELECT "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."project_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_web_vitals_opt_in", + "posthog_team"."autocapture_web_vitals_allowed_metrics", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."session_replay_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."heatmaps_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."modifiers", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" + FROM "posthog_team" + WHERE ("posthog_team"."project_id" = 2 + AND "posthog_team"."id" = 2) + LIMIT 21 + ''' +# --- +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.12 + ''' + SELECT 1 AS "a" + FROM "posthog_grouptypemapping" + WHERE "posthog_grouptypemapping"."team_id" = 2 + LIMIT 1 + ''' +# --- +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.13 + ''' + SELECT "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."project_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_web_vitals_opt_in", + "posthog_team"."autocapture_web_vitals_allowed_metrics", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."session_replay_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."heatmaps_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."modifiers", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" + FROM "posthog_team" + WHERE ("posthog_team"."project_id" = 2 + AND "posthog_team"."id" = 2) + LIMIT 21 + ''' +# --- +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.14 + ''' + SELECT "posthog_user"."id", + "posthog_user"."password", + "posthog_user"."last_login", + "posthog_user"."first_name", + "posthog_user"."last_name", + "posthog_user"."is_staff", + "posthog_user"."is_active", + "posthog_user"."date_joined", + "posthog_user"."uuid", + "posthog_user"."current_organization_id", + "posthog_user"."current_team_id", + "posthog_user"."email", + "posthog_user"."pending_email", + "posthog_user"."temporary_token", + "posthog_user"."distinct_id", + "posthog_user"."is_email_verified", + "posthog_user"."has_seen_product_intro_for", + "posthog_user"."strapi_id", + "posthog_user"."theme_mode", + "posthog_user"."partial_notification_settings", + "posthog_user"."anonymize_data", + "posthog_user"."toolbar_mode", + "posthog_user"."hedgehog_config", + "posthog_user"."events_column_config", + "posthog_user"."email_opt_in" + FROM "posthog_user" + WHERE "posthog_user"."id" = 2 + LIMIT 21 + ''' +# --- +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.15 ''' SELECT "posthog_featureflag"."id", "posthog_featureflag"."key", @@ -86,7 +324,7 @@ AND "posthog_featureflag"."team_id" = 2) ''' # --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.11 +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.16 ''' SELECT "posthog_pluginconfig"."id", "posthog_pluginconfig"."web_token", @@ -176,6 +414,75 @@ ''' # --- # name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.4 + ''' + SELECT "posthog_project"."id", + "posthog_project"."organization_id", + "posthog_project"."name", + "posthog_project"."created_at" + FROM "posthog_project" + WHERE "posthog_project"."id" = 2 + LIMIT 21 + ''' +# --- +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.5 + ''' + SELECT "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."project_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_web_vitals_opt_in", + "posthog_team"."autocapture_web_vitals_allowed_metrics", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."session_replay_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."heatmaps_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."modifiers", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" + FROM "posthog_team" + WHERE ("posthog_team"."project_id" = 2 + AND "posthog_team"."id" = 2) + LIMIT 21 + ''' +# --- +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.6 ''' SELECT "posthog_organizationmembership"."id", "posthog_organizationmembership"."organization_id", @@ -207,7 +514,7 @@ WHERE "posthog_organizationmembership"."user_id" = 2 ''' # --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.5 +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.7 ''' SELECT "posthog_organizationmembership"."id", "posthog_organizationmembership"."organization_id", @@ -239,7 +546,7 @@ WHERE "posthog_organizationmembership"."user_id" = 2 ''' # --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.6 +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.8 ''' SELECT "posthog_team"."id", "posthog_team"."organization_id", @@ -248,26 +555,9 @@ WHERE "posthog_team"."organization_id" IN ('00000000-0000-0000-0000-000000000000'::uuid) ''' # --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.7 +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.9 ''' - SELECT "posthog_hogfunction"."id", - "posthog_hogfunction"."team_id", - "posthog_hogfunction"."name", - "posthog_hogfunction"."description", - "posthog_hogfunction"."created_at", - "posthog_hogfunction"."created_by_id", - "posthog_hogfunction"."deleted", - "posthog_hogfunction"."updated_at", - "posthog_hogfunction"."enabled", - "posthog_hogfunction"."icon_url", - "posthog_hogfunction"."hog", - "posthog_hogfunction"."bytecode", - "posthog_hogfunction"."inputs_schema", - "posthog_hogfunction"."inputs", - "posthog_hogfunction"."filters", - "posthog_hogfunction"."masking", - "posthog_hogfunction"."template_id", - "posthog_team"."id", + SELECT "posthog_team"."id", "posthog_team"."uuid", "posthog_team"."organization_id", "posthog_team"."project_id", @@ -315,58 +605,11 @@ "posthog_team"."modifiers", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."plugins_opt_in", - "posthog_team"."opt_out_capture", - "posthog_team"."event_names", - "posthog_team"."event_names_with_usage", - "posthog_team"."event_properties", - "posthog_team"."event_properties_with_usage", - "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", "posthog_team"."external_data_workspace_last_synced_at" - FROM "posthog_hogfunction" - INNER JOIN "posthog_team" ON ("posthog_hogfunction"."team_id" = "posthog_team"."id") - WHERE ("posthog_hogfunction"."team_id" = 2 - AND "posthog_hogfunction"."filters" @> '{"filter_test_accounts": true}'::jsonb) - ''' -# --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.8 - ''' - SELECT 1 AS "a" - FROM "posthog_grouptypemapping" - WHERE "posthog_grouptypemapping"."team_id" = 2 - LIMIT 1 - ''' -# --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.9 - ''' - SELECT "posthog_user"."id", - "posthog_user"."password", - "posthog_user"."last_login", - "posthog_user"."first_name", - "posthog_user"."last_name", - "posthog_user"."is_staff", - "posthog_user"."is_active", - "posthog_user"."date_joined", - "posthog_user"."uuid", - "posthog_user"."current_organization_id", - "posthog_user"."current_team_id", - "posthog_user"."email", - "posthog_user"."pending_email", - "posthog_user"."temporary_token", - "posthog_user"."distinct_id", - "posthog_user"."is_email_verified", - "posthog_user"."has_seen_product_intro_for", - "posthog_user"."strapi_id", - "posthog_user"."theme_mode", - "posthog_user"."partial_notification_settings", - "posthog_user"."anonymize_data", - "posthog_user"."toolbar_mode", - "posthog_user"."hedgehog_config", - "posthog_user"."events_column_config", - "posthog_user"."email_opt_in" - FROM "posthog_user" - WHERE "posthog_user"."id" = 2 + FROM "posthog_team" + WHERE ("posthog_team"."project_id" = 2 + AND "posthog_team"."id" = 2) LIMIT 21 ''' # --- diff --git a/posthog/api/test/__snapshots__/test_event.ambr b/posthog/api/test/__snapshots__/test_event.ambr index a8aaa175a427e..cae4ce7b8b045 100644 --- a/posthog/api/test/__snapshots__/test_event.ambr +++ b/posthog/api/test/__snapshots__/test_event.ambr @@ -1,5 +1,17 @@ # serializer version: 1 # name: TestEvents.test_event_property_values + ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestEvents.test_event_property_values.1 ''' /* user_id:0 request:_snapshot_ */ SELECT DISTINCT replaceRegexpAll(JSONExtractRaw(properties, 'random_prop'), '^"|"$', '') @@ -11,7 +23,7 @@ LIMIT 10 ''' # --- -# name: TestEvents.test_event_property_values.1 +# name: TestEvents.test_event_property_values.2 ''' /* user_id:0 request:_snapshot_ */ SELECT DISTINCT replaceRegexpAll(JSONExtractRaw(properties, 'random_prop'), '^"|"$', '') @@ -25,7 +37,7 @@ LIMIT 10 ''' # --- -# name: TestEvents.test_event_property_values.2 +# name: TestEvents.test_event_property_values.3 ''' /* user_id:0 request:_snapshot_ */ SELECT DISTINCT replaceRegexpAll(JSONExtractRaw(properties, 'random_prop'), '^"|"$', '') @@ -39,7 +51,7 @@ LIMIT 10 ''' # --- -# name: TestEvents.test_event_property_values.3 +# name: TestEvents.test_event_property_values.4 ''' /* user_id:0 request:_snapshot_ */ SELECT DISTINCT replaceRegexpAll(JSONExtractRaw(properties, 'random_prop'), '^"|"$', '') @@ -53,7 +65,7 @@ LIMIT 10 ''' # --- -# name: TestEvents.test_event_property_values.4 +# name: TestEvents.test_event_property_values.5 ''' /* user_id:0 request:_snapshot_ */ SELECT DISTINCT replaceRegexpAll(JSONExtractRaw(properties, 'random_prop'), '^"|"$', '') @@ -68,7 +80,7 @@ LIMIT 10 ''' # --- -# name: TestEvents.test_event_property_values.5 +# name: TestEvents.test_event_property_values.6 ''' /* user_id:0 request:_snapshot_ */ SELECT DISTINCT replaceRegexpAll(JSONExtractRaw(properties, 'random_prop'), '^"|"$', '') @@ -84,7 +96,7 @@ LIMIT 10 ''' # --- -# name: TestEvents.test_event_property_values.6 +# name: TestEvents.test_event_property_values.7 ''' /* user_id:0 request:_snapshot_ */ SELECT DISTINCT replaceRegexpAll(JSONExtractRaw(properties, 'random_prop'), '^"|"$', '') @@ -100,6 +112,18 @@ ''' # --- # name: TestEvents.test_event_property_values_materialized + ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestEvents.test_event_property_values_materialized.1 ''' /* user_id:0 request:_snapshot_ */ SELECT DISTINCT "mat_random_prop" @@ -111,7 +135,7 @@ LIMIT 10 ''' # --- -# name: TestEvents.test_event_property_values_materialized.1 +# name: TestEvents.test_event_property_values_materialized.2 ''' /* user_id:0 request:_snapshot_ */ SELECT DISTINCT "mat_random_prop" @@ -125,7 +149,7 @@ LIMIT 10 ''' # --- -# name: TestEvents.test_event_property_values_materialized.2 +# name: TestEvents.test_event_property_values_materialized.3 ''' /* user_id:0 request:_snapshot_ */ SELECT DISTINCT "mat_random_prop" @@ -139,7 +163,7 @@ LIMIT 10 ''' # --- -# name: TestEvents.test_event_property_values_materialized.3 +# name: TestEvents.test_event_property_values_materialized.4 ''' /* user_id:0 request:_snapshot_ */ SELECT DISTINCT "mat_random_prop" @@ -153,7 +177,7 @@ LIMIT 10 ''' # --- -# name: TestEvents.test_event_property_values_materialized.4 +# name: TestEvents.test_event_property_values_materialized.5 ''' /* user_id:0 request:_snapshot_ */ SELECT DISTINCT "mat_random_prop" @@ -168,7 +192,7 @@ LIMIT 10 ''' # --- -# name: TestEvents.test_event_property_values_materialized.5 +# name: TestEvents.test_event_property_values_materialized.6 ''' /* user_id:0 request:_snapshot_ */ SELECT DISTINCT "mat_random_prop" @@ -184,7 +208,7 @@ LIMIT 10 ''' # --- -# name: TestEvents.test_event_property_values_materialized.6 +# name: TestEvents.test_event_property_values_materialized.7 ''' /* user_id:0 request:_snapshot_ */ SELECT DISTINCT "mat_random_prop" diff --git a/posthog/api/test/test_decide.py b/posthog/api/test/test_decide.py index 95fdd27717c41..2bd47011c93c8 100644 --- a/posthog/api/test/test_decide.py +++ b/posthog/api/test/test_decide.py @@ -2665,6 +2665,7 @@ def test_short_circuited_team(self, *args): ], "has_completed_onboarding_for": {"product_analytics": True}, }, + initiating_user=self.user, ) with self.settings(DECIDE_SHORT_CIRCUITED_TEAM_IDS=[short_circuited_team.id]): response = self._post_decide( diff --git a/posthog/api/test/test_project.py b/posthog/api/test/test_project.py new file mode 100644 index 0000000000000..a3da3c81f9ce0 --- /dev/null +++ b/posthog/api/test/test_project.py @@ -0,0 +1,11 @@ +from posthog.api.test.test_team import EnvironmentToProjectRewriteClient, team_api_test_factory + + +class TestProjectAPI(team_api_test_factory()): # type: ignore + """ + We inherit from TestTeamAPI, as previously /api/projects/ referred to the Team model, which used to mean "project". + Now as Team means "environment" and Project is separate, we must ensure backward compatibility of /api/projects/. + At the same time, this class is where we can continue adding `Project`-specific API tests. + """ + + client_class = EnvironmentToProjectRewriteClient diff --git a/posthog/api/test/test_routing.py b/posthog/api/test/test_routing.py index 76abb2693cf92..24063d1a655e5 100644 --- a/posthog/api/test/test_routing.py +++ b/posthog/api/test/test_routing.py @@ -2,9 +2,74 @@ from posthog.api.routing import TeamAndOrgViewSetMixin +from django.test import override_settings +from django.urls import include, path +from rest_framework import viewsets +from posthog.api.annotation import AnnotationSerializer +from posthog.api.routing import DefaultRouterPlusPlus +from posthog.models.annotation import Annotation +from posthog.models.organization import Organization +from posthog.models.project import Project +from posthog.models.team.team import Team +from posthog.test.base import APIBaseTest -class TestRouting: +class FooViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): + scope_object = "INTERNAL" + queryset = Annotation.objects.all() + serializer_class = AnnotationSerializer + + +test_router = DefaultRouterPlusPlus() + +test_environments_router = test_router.register(r"environments", FooViewSet, "environments") +test_environments_router.register(r"foos", FooViewSet, "environment_foos", ["team_id"]) + +test_projects_router = test_router.register(r"projects", FooViewSet, "projects") +test_projects_router.register(r"foos", FooViewSet, "project_foos", ["project_id"]) + +test_organizations_router = test_router.register(r"organizations", FooViewSet, "organizations") +test_organizations_router.register(r"foos", FooViewSet, "organization_foos", ["organization_id"]) + + +urlpatterns = [ + path("api/", include(test_router.urls)), +] + + +@override_settings(ROOT_URLCONF=__name__) # Use `urlpatterns` from this file and not from `posthog.urls` +class TestTeamAndOrgViewSetMixin(APIBaseTest): + test_annotation: Annotation + + def setUp(self): + super().setUp() + other_org, _, other_org_team = Organization.objects.bootstrap(user=self.user) + self.other_org_annotation = Annotation.objects.create(team=other_org_team, organization=other_org) + _, other_project_team = Project.objects.create_with_team( + initiating_user=self.user, organization=self.organization + ) + self.other_project_annotation = Annotation.objects.create( + team=other_project_team, organization=self.organization + ) + other_team = Team.objects.create(organization=self.organization, project=self.project) + self.other_team_annotation = Annotation.objects.create(team=other_team, organization=self.organization) + self.current_team_annotation = Annotation.objects.create(team=self.team, organization=self.organization) + + def test_environment_nested_filtering(self): + response = self.client.get(f"/api/environments/{self.team.id}/foos/") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["count"], 1) # Just current_team_annotation + + def test_project_nested_filtering(self): + response = self.client.get(f"/api/projects/{self.team.id}/foos/") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["count"], 2) # Both current_team_annotation and other_team_annotation + + def test_organization_nested_filtering(self): + response = self.client.get(f"/api/organizations/{self.organization.id}/foos/") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["count"], 3) # All except other_org_annotation + def test_cannot_override_special_methods(self): with pytest.raises(Exception) as e: diff --git a/posthog/api/test/test_team.py b/posthog/api/test/test_team.py index 04c3787ad25a1..3e4c3e48d2dad 100644 --- a/posthog/api/test/test_team.py +++ b/posthog/api/test/test_team.py @@ -4,12 +4,11 @@ from unittest import mock from unittest.mock import ANY, MagicMock, call, patch -from asgiref.sync import sync_to_async from django.core.cache import cache from django.http import HttpResponse from freezegun import freeze_time from parameterized import parameterized -from rest_framework import status +from rest_framework import status, test from temporalio.service import RPCError from posthog.api.test.batch_exports.conftest import start_test_worker @@ -26,297 +25,341 @@ from posthog.test.base import APIBaseTest -class TestTeamAPI(APIBaseTest): - def _assert_activity_log(self, expected: list[dict], team_id: Optional[int] = None) -> None: - if not team_id: - team_id = self.team.pk - - starting_log_response = self.client.get(f"/api/projects/{team_id}/activity") - assert starting_log_response.status_code == 200, starting_log_response.json() - assert starting_log_response.json()["results"] == expected - - def _assert_organization_activity_log(self, expected: list[dict]) -> None: - starting_log_response = self.client.get(f"/api/organizations/{self.organization.pk}/activity") - assert starting_log_response.status_code == 200, starting_log_response.json() - assert starting_log_response.json()["results"] == expected - - def _assert_activity_log_is_empty(self) -> None: - self._assert_activity_log([]) - - def test_list_projects(self): - response = self.client.get("/api/projects/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - - # Listing endpoint always uses the simplified serializer - response_data = response.json() - self.assertEqual(len(response_data["results"]), 1) - self.assertEqual(response_data["results"][0]["name"], self.team.name) - self.assertNotIn("test_account_filters", response_data["results"][0]) - self.assertNotIn("data_attributes", response_data["results"][0]) - - # TODO: These assertions will no longer make sense when we fully remove these attributes from the model - self.assertNotIn("event_names", response_data["results"][0]) - self.assertNotIn("event_properties", response_data["results"][0]) - self.assertNotIn("event_properties_numerical", response_data["results"][0]) - - def test_retrieve_project(self): - response = self.client.get("/api/projects/@current/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - - response_data = response.json() - self.assertEqual(response_data["name"], self.team.name) - self.assertEqual(response_data["timezone"], "UTC") - self.assertEqual(response_data["is_demo"], False) - self.assertEqual(response_data["slack_incoming_webhook"], self.team.slack_incoming_webhook) - self.assertEqual(response_data["has_group_types"], False) - self.assertEqual( - response_data["person_on_events_querying_enabled"], - get_instance_setting("PERSON_ON_EVENTS_ENABLED") or get_instance_setting("PERSON_ON_EVENTS_V2_ENABLED"), - ) +def team_api_test_factory(): + class TestTeamAPI(APIBaseTest): + """Tests for /api/environments/.""" + + def _assert_activity_log(self, expected: list[dict], team_id: Optional[int] = None) -> None: + if not team_id: + team_id = self.team.pk + + starting_log_response = self.client.get(f"/api/environments/{team_id}/activity") + assert starting_log_response.status_code == 200, starting_log_response.json() + assert starting_log_response.json()["results"] == expected + + def _assert_organization_activity_log(self, expected: list[dict]) -> None: + starting_log_response = self.client.get(f"/api/organizations/{self.organization.pk}/activity") + assert starting_log_response.status_code == 200, starting_log_response.json() + assert starting_log_response.json()["results"] == expected + + def _assert_activity_log_is_empty(self) -> None: + self._assert_activity_log([]) + + def test_list_teams(self): + response = self.client.get("/api/environments/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Listing endpoint always uses the simplified serializer + response_data = response.json() + self.assertEqual(len(response_data["results"]), 1) + self.assertEqual(response_data["results"][0]["name"], self.team.name) + self.assertNotIn("test_account_filters", response_data["results"][0]) + self.assertNotIn("data_attributes", response_data["results"][0]) + + # TODO: These assertions will no longer make sense when we fully remove these attributes from the model + self.assertNotIn("event_names", response_data["results"][0]) + self.assertNotIn("event_properties", response_data["results"][0]) + self.assertNotIn("event_properties_numerical", response_data["results"][0]) + + def test_retrieve_team(self): + response = self.client.get("/api/environments/@current/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response_data = response.json() + self.assertEqual(response_data["name"], self.team.name) + self.assertEqual(response_data["timezone"], "UTC") + self.assertEqual(response_data["is_demo"], False) + self.assertEqual(response_data["slack_incoming_webhook"], self.team.slack_incoming_webhook) + self.assertEqual(response_data["has_group_types"], False) + self.assertEqual( + response_data["person_on_events_querying_enabled"], + get_instance_setting("PERSON_ON_EVENTS_ENABLED") or get_instance_setting("PERSON_ON_EVENTS_V2_ENABLED"), + ) - # TODO: These assertions will no longer make sense when we fully remove these attributes from the model - self.assertNotIn("event_names", response_data) - self.assertNotIn("event_properties", response_data) - self.assertNotIn("event_properties_numerical", response_data) - self.assertNotIn("event_names_with_usage", response_data) - self.assertNotIn("event_properties_with_usage", response_data) - - def test_cant_retrieve_project_from_another_org(self): - org = Organization.objects.create(name="New Org") - team = Team.objects.create(organization=org, name="Default project") - - response = self.client.get(f"/api/projects/{team.pk}/") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertEqual(response.json(), self.not_found_response()) - - @patch("posthog.api.team.get_geoip_properties") - def test_ip_location_is_used_for_new_project_week_day_start(self, get_geoip_properties_mock: MagicMock): - self.organization.available_product_features = [ - {"key": AvailableFeature.ORGANIZATIONS_PROJECTS, "name": AvailableFeature.ORGANIZATIONS_PROJECTS} - ] - self.organization.save() - self.organization_membership.level = OrganizationMembership.Level.ADMIN - self.organization_membership.save() - - get_geoip_properties_mock.return_value = {} - response = self.client.post("/api/projects/", {"name": "Test World"}) - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.json()) - self.assertDictContainsSubset({"name": "Test World", "week_start_day": None}, response.json()) - - get_geoip_properties_mock.return_value = {"$geoip_country_code": "US"} - response = self.client.post("/api/projects/", {"name": "Test US"}) - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.json()) - self.assertDictContainsSubset({"name": "Test US", "week_start_day": 0}, response.json()) - - get_geoip_properties_mock.return_value = {"$geoip_country_code": "PL"} - response = self.client.post("/api/projects/", {"name": "Test PL"}) - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.json()) - self.assertDictContainsSubset({"name": "Test PL", "week_start_day": 1}, response.json()) - - get_geoip_properties_mock.return_value = {"$geoip_country_code": "IR"} - response = self.client.post("/api/projects/", {"name": "Test IR"}) - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.json()) - self.assertDictContainsSubset({"name": "Test IR", "week_start_day": 0}, response.json()) - - def test_cant_create_team_without_license_on_selfhosted(self): - with self.is_cloud(False): - response = self.client.post("/api/projects/", {"name": "Test"}) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + # TODO: These assertions will no longer make sense when we fully remove these attributes from the model + self.assertNotIn("event_names", response_data) + self.assertNotIn("event_properties", response_data) + self.assertNotIn("event_properties_numerical", response_data) + self.assertNotIn("event_names_with_usage", response_data) + self.assertNotIn("event_properties_with_usage", response_data) + + def test_cant_retrieve_team_from_another_org(self): + org = Organization.objects.create(name="New Org") + team = Team.objects.create(organization=org, name="Default project") + + response = self.client.get(f"/api/environments/{team.pk}/") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.json(), self.not_found_response()) + + @patch("posthog.api.project.get_geoip_properties") + @patch("posthog.api.team.get_geoip_properties") + def test_ip_location_is_used_for_new_team_week_day_start( + self, get_geoip_properties_mock: MagicMock, get_geoip_properties_legacy_endpoint: MagicMock + ): + if self.client_class is EnvironmentToProjectRewriteClient: + get_geoip_properties_mock = get_geoip_properties_legacy_endpoint + + self.organization.available_product_features = [ + {"key": AvailableFeature.ORGANIZATIONS_PROJECTS, "name": AvailableFeature.ORGANIZATIONS_PROJECTS} + ] + self.organization.save() + self.organization_membership.level = OrganizationMembership.Level.ADMIN + self.organization_membership.save() + + get_geoip_properties_mock.return_value = {} + response = self.client.post("/api/environments/", {"name": "Test World"}) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.json()) + self.assertDictContainsSubset({"name": "Test World", "week_start_day": None}, response.json()) + + get_geoip_properties_mock.return_value = {"$geoip_country_code": "US"} + response = self.client.post("/api/environments/", {"name": "Test US"}) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.json()) + self.assertDictContainsSubset({"name": "Test US", "week_start_day": 0}, response.json()) + + get_geoip_properties_mock.return_value = {"$geoip_country_code": "PL"} + response = self.client.post("/api/environments/", {"name": "Test PL"}) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.json()) + self.assertDictContainsSubset({"name": "Test PL", "week_start_day": 1}, response.json()) + + get_geoip_properties_mock.return_value = {"$geoip_country_code": "IR"} + response = self.client.post("/api/environments/", {"name": "Test IR"}) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.json()) + self.assertDictContainsSubset({"name": "Test IR", "week_start_day": 0}, response.json()) + + def test_cant_create_team_without_license_on_selfhosted(self): + with self.is_cloud(False): + response = self.client.post("/api/environments/", {"name": "Test"}) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Team.objects.count(), 1) + response = self.client.post("/api/environments/", {"name": "Test"}) + self.assertEqual(Team.objects.count(), 1) + + def test_cant_create_a_second_team_without_license(self): + self.organization_membership.level = OrganizationMembership.Level.ADMIN + self.organization_membership.save() self.assertEqual(Team.objects.count(), 1) - response = self.client.post("/api/projects/", {"name": "Test"}) + + response = self.client.post("/api/environments/", {"name": "Hedgebox", "is_demo": False}) + self.assertEqual(response.status_code, 403) + response_data = response.json() + self.assertDictContainsSubset( + { + "type": "authentication_error", + "code": "permission_denied", + "detail": "You must upgrade your PostHog plan to be able to create and manage multiple projects or environments.", + }, + response_data, + ) self.assertEqual(Team.objects.count(), 1) - def test_cant_create_a_second_project_without_license(self): - self.organization_membership.level = OrganizationMembership.Level.ADMIN - self.organization_membership.save() - self.assertEqual(Team.objects.count(), 1) - - response = self.client.post("/api/projects/", {"name": "Hedgebox", "is_demo": False}) - self.assertEqual(response.status_code, 403) - response_data = response.json() - self.assertDictContainsSubset( - { - "type": "authentication_error", - "code": "permission_denied", - "detail": "You must upgrade your PostHog plan to be able to create and manage multiple projects.", - }, - response_data, - ) - self.assertEqual(Team.objects.count(), 1) - - # another request without the is_demo parameter - response = self.client.post("/api/projects/", {"name": "Hedgebox"}) - self.assertEqual(response.status_code, 403) - response_data = response.json() - self.assertDictContainsSubset( - { - "type": "authentication_error", - "code": "permission_denied", - "detail": "You must upgrade your PostHog plan to be able to create and manage multiple projects.", - }, - response_data, - ) - self.assertEqual(Team.objects.count(), 1) + # another request without the is_demo parameter + response = self.client.post("/api/environments/", {"name": "Hedgebox"}) + self.assertEqual(response.status_code, 403) + response_data = response.json() + self.assertDictContainsSubset( + { + "type": "authentication_error", + "code": "permission_denied", + "detail": "You must upgrade your PostHog plan to be able to create and manage multiple projects or environments.", + }, + response_data, + ) + self.assertEqual(Team.objects.count(), 1) - @freeze_time("2022-02-08") - def test_update_project_timezone(self): - self._assert_activity_log_is_empty() + @freeze_time("2022-02-08") + def test_update_team_timezone(self): + self._assert_activity_log_is_empty() + + response = self.client.patch("/api/environments/@current/", {"timezone": "Europe/Lisbon"}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response_data = response.json() + self.assertEqual(response_data["name"], self.team.name) + self.assertEqual(response_data["timezone"], "Europe/Lisbon") + + self.team.refresh_from_db() + self.assertEqual(self.team.timezone, "Europe/Lisbon") + + self._assert_activity_log( + [ + { + "activity": "updated", + "created_at": "2022-02-08T00:00:00Z", + "detail": { + "changes": [ + { + "action": "changed", + "after": "Europe/Lisbon", + "before": "UTC", + "field": "timezone", + "type": "Team", + }, + ], + "name": "Default project", + "short_id": None, + "trigger": None, + "type": None, + }, + "item_id": str(self.team.pk), + "scope": "Team", + "user": { + "email": "user1@posthog.com", + "first_name": "", + }, + }, + ] + ) - response = self.client.patch("/api/projects/@current/", {"timezone": "Europe/Lisbon"}) - self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_update_test_filter_default_checked(self): + response = self.client.patch( + "/api/environments/@current/", {"test_account_filters_default_checked": "true"} + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) - response_data = response.json() - self.assertEqual(response_data["name"], self.team.name) - self.assertEqual(response_data["timezone"], "Europe/Lisbon") + response_data = response.json() + self.assertEqual(response_data["test_account_filters_default_checked"], True) - self.team.refresh_from_db() - self.assertEqual(self.team.timezone, "Europe/Lisbon") + self.team.refresh_from_db() + self.assertEqual(self.team.test_account_filters_default_checked, True) - self._assert_activity_log( - [ + def test_cannot_set_invalid_timezone_for_team(self): + response = self.client.patch("/api/environments/@current/", {"timezone": "America/I_Dont_Exist"}) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + response.json(), + { + "type": "validation_error", + "code": "invalid_choice", + "detail": '"America/I_Dont_Exist" is not a valid choice.', + "attr": "timezone", + }, + ) + + self.team.refresh_from_db() + self.assertNotEqual(self.team.timezone, "America/I_Dont_Exist") + + def test_cant_update_team_from_another_org(self): + org = Organization.objects.create(name="New Org") + team = Team.objects.create(organization=org, name="Default project") + + response = self.client.patch(f"/api/environments/{team.pk}/", {"timezone": "Africa/Accra"}) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.json(), self.not_found_response()) + + team.refresh_from_db() + self.assertEqual(team.timezone, "UTC") + + def test_filter_permission(self): + response = self.client.patch( + f"/api/environments/{self.team.id}/", + {"test_account_filters": [{"key": "$current_url", "value": "test"}]}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response_data = response.json() + self.assertEqual(response_data["name"], self.team.name) + self.assertEqual( + response_data["test_account_filters"], + [{"key": "$current_url", "value": "test"}], + ) + + @freeze_time("2022-02-08") + def test_delete_team_activity_log(self): + self.organization_membership.level = OrganizationMembership.Level.ADMIN + self.organization_membership.save() + + team: Team = Team.objects.create_with_data(initiating_user=self.user, organization=self.organization) + + response = self.client.delete(f"/api/environments/{team.id}") + assert response.status_code == 204 + + # activity log is queried in the context of the team + # and the team was deleted, so we can't (for now) view a deleted team activity via the API + # even though the activity log is recorded + + deleted_team_activity_response = self.client.get(f"/api/environments/{team.id}/activity") + assert deleted_team_activity_response.status_code == status.HTTP_404_NOT_FOUND + + # we can't query by API but can prove the log was recorded + activity = [a.__dict__ for a in ActivityLog.objects.filter(team_id=team.pk).all()] + expected_activity = [ { - "activity": "updated", - "created_at": "2022-02-08T00:00:00Z", + "_state": ANY, + "activity": "deleted", + "created_at": ANY, "detail": { - "changes": [ - { - "action": "changed", - "after": "Europe/Lisbon", - "before": "UTC", - "field": "timezone", - "type": "Team", - }, - ], + "changes": None, "name": "Default project", "short_id": None, "trigger": None, "type": None, }, - "item_id": str(self.team.pk), + "id": ANY, + "is_system": False, + "organization_id": ANY, + "team_id": team.pk, + "item_id": str(team.pk), "scope": "Team", - "user": { - "email": "user1@posthog.com", - "first_name": "", - }, + "user_id": self.user.pk, + "was_impersonated": False, }, ] - ) - - def test_update_test_filter_default_checked(self): - response = self.client.patch("/api/projects/@current/", {"test_account_filters_default_checked": "true"}) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - response_data = response.json() - self.assertEqual(response_data["test_account_filters_default_checked"], True) - - self.team.refresh_from_db() - self.assertEqual(self.team.test_account_filters_default_checked, True) - - def test_cannot_set_invalid_timezone_for_project(self): - response = self.client.patch("/api/projects/@current/", {"timezone": "America/I_Dont_Exist"}) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.json(), - { - "type": "validation_error", - "code": "invalid_choice", - "detail": '"America/I_Dont_Exist" is not a valid choice.', - "attr": "timezone", - }, - ) - - self.team.refresh_from_db() - self.assertNotEqual(self.team.timezone, "America/I_Dont_Exist") + if self.client_class is EnvironmentToProjectRewriteClient: + expected_activity.insert( + 0, + { + "_state": ANY, + "activity": "deleted", + "created_at": ANY, + "detail": { + "changes": None, + "name": "Default project", + "short_id": None, + "trigger": None, + "type": None, + }, + "id": ANY, + "is_system": False, + "organization_id": ANY, + "team_id": team.pk, + "item_id": str(team.project_id), + "scope": "Project", + "user_id": self.user.pk, + "was_impersonated": False, + }, + ) + assert activity == expected_activity - def test_cant_update_project_from_another_org(self): - org = Organization.objects.create(name="New Org") - team = Team.objects.create(organization=org, name="Default project") + @patch("posthog.api.project.delete_bulky_postgres_data") + @patch("posthog.api.team.delete_bulky_postgres_data") + @patch("posthoganalytics.capture") + def test_delete_team_own_second( + self, + mock_capture: MagicMock, + mock_delete_bulky_postgres_data: MagicMock, + mock_delete_bulky_postgres_data_legacy_endpoint: MagicMock, + ): + if self.client_class is EnvironmentToProjectRewriteClient: + mock_delete_bulky_postgres_data = mock_delete_bulky_postgres_data_legacy_endpoint - response = self.client.patch(f"/api/projects/{team.pk}/", {"timezone": "Africa/Accra"}) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertEqual(response.json(), self.not_found_response()) + self.organization_membership.level = OrganizationMembership.Level.ADMIN + self.organization_membership.save() - team.refresh_from_db() - self.assertEqual(team.timezone, "UTC") + team: Team = Team.objects.create_with_data(initiating_user=self.user, organization=self.organization) - def test_filter_permission(self): - response = self.client.patch( - f"/api/projects/{self.team.id}/", - {"test_account_filters": [{"key": "$current_url", "value": "test"}]}, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(Team.objects.filter(organization=self.organization).count(), 2) - response_data = response.json() - self.assertEqual(response_data["name"], self.team.name) - self.assertEqual( - response_data["test_account_filters"], - [{"key": "$current_url", "value": "test"}], - ) + response = self.client.delete(f"/api/environments/{team.id}") - @freeze_time("2022-02-08") - @patch("posthog.api.team.delete_bulky_postgres_data") - @patch("posthoganalytics.capture") - def test_delete_team_activity_log(self, mock_capture: MagicMock, mock_delete_bulky_postgres_data: MagicMock): - self.organization_membership.level = OrganizationMembership.Level.ADMIN - self.organization_membership.save() - - team: Team = Team.objects.create_with_data(organization=self.organization) - - response = self.client.delete(f"/api/projects/{team.id}") - assert response.status_code == 204 - - # activity log is queried in the context of the team - # and the team was deleted, so we can't (for now) view a deleted team activity via the API - # even though the activity log is recorded - - deleted_team_activity_response = self.client.get(f"/api/projects/{team.id}/activity") - assert deleted_team_activity_response.status_code == status.HTTP_404_NOT_FOUND - - # we can't query by API but can prove the log was recorded - activity = [a.__dict__ for a in ActivityLog.objects.filter(team_id=team.pk).all()] - assert activity == [ - { - "_state": ANY, - "activity": "deleted", - "created_at": ANY, - "detail": { - "changes": None, - "name": "Default project", - "short_id": None, - "trigger": None, - "type": None, - }, - "id": ANY, - "is_system": False, - "organization_id": ANY, - "team_id": team.pk, - "item_id": str(team.pk), - "scope": "Team", - "user_id": self.user.pk, - "was_impersonated": False, - }, - ] - - @patch("posthog.api.team.delete_bulky_postgres_data") - @patch("posthoganalytics.capture") - def test_delete_team_own_second(self, mock_capture: MagicMock, mock_delete_bulky_postgres_data: MagicMock): - self.organization_membership.level = OrganizationMembership.Level.ADMIN - self.organization_membership.save() - - team: Team = Team.objects.create_with_data(organization=self.organization) - - self.assertEqual(Team.objects.filter(organization=self.organization).count(), 2) - - response = self.client.delete(f"/api/projects/{team.id}") - - self.assertEqual(response.status_code, 204) - self.assertEqual(Team.objects.filter(organization=self.organization).count(), 1) - self.assertEqual( - AsyncDeletion.objects.filter(team_id=team.id, deletion_type=DeletionType.Team, key=str(team.id)).count(), - 1, - ) - mock_capture.assert_has_calls( - calls=[ + self.assertEqual(response.status_code, 204) + self.assertEqual(Team.objects.filter(organization=self.organization).count(), 1) + self.assertEqual( + AsyncDeletion.objects.filter( + team_id=team.id, deletion_type=DeletionType.Team, key=str(team.id) + ).count(), + 1, + ) + expected_capture_calls = [ call( self.user.distinct_id, "membership level changed", @@ -325,722 +368,768 @@ def test_delete_team_own_second(self, mock_capture: MagicMock, mock_delete_bulky ), call(self.user.distinct_id, "team deleted", properties={}, groups=mock.ANY), ] - ) - mock_delete_bulky_postgres_data.assert_called_once_with(team_ids=[team.pk]) + if self.client_class is EnvironmentToProjectRewriteClient: + expected_capture_calls.append( + call( + self.user.distinct_id, + "project deleted", + properties={"project_name": "Default project"}, + groups=mock.ANY, + ) + ) + assert mock_capture.call_args_list == expected_capture_calls + mock_delete_bulky_postgres_data.assert_called_once_with(team_ids=[team.pk]) + + def test_delete_bulky_postgres_data(self): + self.organization_membership.level = OrganizationMembership.Level.ADMIN + self.organization_membership.save() + + team: Team = Team.objects.create_with_data(initiating_user=self.user, organization=self.organization) + + self.assertEqual(Team.objects.filter(organization=self.organization).count(), 2) + + from posthog.models.cohort import Cohort, CohortPeople + from posthog.models.feature_flag.feature_flag import ( + FeatureFlag, + FeatureFlagHashKeyOverride, + ) - def test_delete_bulky_postgres_data(self): - self.organization_membership.level = OrganizationMembership.Level.ADMIN - self.organization_membership.save() + # from posthog.models.insight_caching_state import InsightCachingState + from posthog.models.person import Person - team: Team = Team.objects.create_with_data(organization=self.organization) + cohort = Cohort.objects.create(team=team, created_by=self.user, name="test") + person = Person.objects.create( + team=team, + distinct_ids=["example_id"], + properties={"email": "tim@posthog.com", "team": "posthog"}, + ) + person.add_distinct_id("test") + flag = FeatureFlag.objects.create( + team=team, + name="test", + key="test", + rollout_percentage=50, + created_by=self.user, + ) + FeatureFlagHashKeyOverride.objects.create( + team_id=team.pk, + person_id=person.id, + feature_flag_key=flag.key, + hash_key="test", + ) + CohortPeople.objects.create(cohort_id=cohort.pk, person_id=person.pk) + EarlyAccessFeature.objects.create( + team=team, + name="Test flag", + description="A fancy new flag.", + stage="beta", + feature_flag=flag, + ) - self.assertEqual(Team.objects.filter(organization=self.organization).count(), 2) + # if something is missing then teardown fails + response = self.client.delete(f"/api/environments/{team.id}") + self.assertEqual(response.status_code, 204) - from posthog.models.cohort import Cohort, CohortPeople - from posthog.models.feature_flag.feature_flag import ( - FeatureFlag, - FeatureFlagHashKeyOverride, - ) + def test_delete_batch_exports(self): + self.organization_membership.level = OrganizationMembership.Level.ADMIN + self.organization_membership.save() - # from posthog.models.insight_caching_state import InsightCachingState - from posthog.models.person import Person + team: Team = Team.objects.create_with_data(initiating_user=self.user, organization=self.organization) - cohort = Cohort.objects.create(team=team, created_by=self.user, name="test") - person = Person.objects.create( - team=team, - distinct_ids=["example_id"], - properties={"email": "tim@posthog.com", "team": "posthog"}, - ) - person.add_distinct_id("test") - flag = FeatureFlag.objects.create( - team=team, - name="test", - key="test", - rollout_percentage=50, - created_by=self.user, - ) - FeatureFlagHashKeyOverride.objects.create( - team_id=team.pk, - person_id=person.id, - feature_flag_key=flag.key, - hash_key="test", - ) - CohortPeople.objects.create(cohort_id=cohort.pk, person_id=person.pk) - EarlyAccessFeature.objects.create( - team=team, - name="Test flag", - description="A fancy new flag.", - stage="beta", - feature_flag=flag, - ) + destination_data = { + "type": "S3", + "config": { + "bucket_name": "my-production-s3-bucket", + "region": "us-east-1", + "prefix": "posthog-events/", + "aws_access_key_id": "abc123", + "aws_secret_access_key": "secret", + }, + } + + batch_export_data = { + "name": "my-production-s3-bucket-destination", + "destination": destination_data, + "interval": "hour", + } + + temporal = sync_connect() + + with start_test_worker(temporal): + response = self.client.post( + f"/api/environments/{team.id}/batch_exports", + json.dumps(batch_export_data), + content_type="application/json", + ) + self.assertEqual(response.status_code, 201) + + batch_export = response.json() + batch_export_id = batch_export["id"] + + response = self.client.delete(f"/api/environments/{team.id}") + self.assertEqual(response.status_code, 204) + + response = self.client.get(f"/api/environments/{team.id}/batch_exports/{batch_export_id}") + self.assertEqual(response.status_code, 404) + + with self.assertRaises(RPCError): + describe_schedule(temporal, batch_export_id) + + @freeze_time("2022-02-08") + def test_reset_token(self): + self.organization_membership.level = OrganizationMembership.Level.ADMIN + self.organization_membership.save() + + self._assert_activity_log_is_empty() + + self.team.api_token = "xyz" + self.team.save() + + response = self.client.patch(f"/api/environments/{self.team.id}/reset_token/") + response_data = response.json() + + self.team.refresh_from_db() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertNotEqual(response_data["api_token"], "xyz") + self.assertEqual(response_data["api_token"], self.team.api_token) + self.assertTrue(response_data["api_token"].startswith("phc_")) + + self._assert_activity_log( + [ + { + "activity": "updated", + "created_at": "2022-02-08T00:00:00Z", + "detail": { + "changes": [ + { + "action": "changed", + "after": None, + "before": None, + "field": "api_token", + "type": "Team", + }, + ], + "name": "Default project", + "short_id": None, + "trigger": None, + "type": None, + }, + "item_id": str(self.team.pk), + "scope": "Team", + "user": { + "email": "user1@posthog.com", + "first_name": "", + }, + }, + ] + ) + + def test_reset_token_insufficient_priviledges(self): + self.team.api_token = "xyz" + self.team.save() + + response = self.client.patch(f"/api/environments/{self.team.id}/reset_token/") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_update_primary_dashboard(self): + d = Dashboard.objects.create(name="Test", team=self.team) + + # Can set it + response = self.client.patch("/api/environments/@current/", {"primary_dashboard": d.id}) + response_data = response.json() + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response_data["name"], self.team.name) + self.assertEqual(response_data["primary_dashboard"], d.id) + + def test_cant_set_primary_dashboard_to_another_teams_dashboard(self): + self.team.primary_dashboard_id = None # Remove the default primary dashboard from the picture + self.team.save() + + team_2 = Team.objects.create(organization=self.organization, name="Default project") + d = Dashboard.objects.create(name="Test", team=team_2) - # if something is missing then teardown fails - response = self.client.delete(f"/api/projects/{team.id}") - self.assertEqual(response.status_code, 204) - - def test_delete_batch_exports(self): - self.organization_membership.level = OrganizationMembership.Level.ADMIN - self.organization_membership.save() - - team: Team = Team.objects.create_with_data(organization=self.organization) - - destination_data = { - "type": "S3", - "config": { - "bucket_name": "my-production-s3-bucket", - "region": "us-east-1", - "prefix": "posthog-events/", - "aws_access_key_id": "abc123", - "aws_secret_access_key": "secret", - }, - } - - batch_export_data = { - "name": "my-production-s3-bucket-destination", - "destination": destination_data, - "interval": "hour", - } - - temporal = sync_connect() - - with start_test_worker(temporal): - response = self.client.post( - f"/api/projects/{team.id}/batch_exports", - json.dumps(batch_export_data), - content_type="application/json", + response = self.client.patch("/api/environments/@current/", {"primary_dashboard": d.id}) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + response = self.client.get("/api/environments/@current/") + response_data = response.json() + self.assertEqual(response_data["primary_dashboard"], None) + + def test_is_generating_demo_data(self): + cache_key = f"is_generating_demo_data_{self.team.pk}" + cache.set(cache_key, "True") + response = self.client.get(f"/api/environments/{self.team.id}/is_generating_demo_data/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json(), {"is_generating_demo_data": True}) + cache.delete(cache_key) + response = self.client.get(f"/api/environments/{self.team.id}/is_generating_demo_data/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json(), {"is_generating_demo_data": False}) + + @patch("posthog.tasks.demo_create_data.create_data_for_demo_team.delay") + def test_org_member_can_create_demo_project(self, mock_create_data_for_demo_team: MagicMock): + self.organization_membership.level = OrganizationMembership.Level.MEMBER + self.organization_membership.save() + response = self.client.post("/api/environments/", {"name": "Hedgebox", "is_demo": True}) + mock_create_data_for_demo_team.assert_called_once() + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + @freeze_time("2022-02-08") + def test_team_float_config_can_be_serialized_to_activity_log(self): + # regression test since this isn't true by default + response = self.client.patch(f"/api/environments/@current/", {"session_recording_sample_rate": 0.4}) + assert response.status_code == status.HTTP_200_OK + self._assert_activity_log( + [ + { + "activity": "updated", + "created_at": "2022-02-08T00:00:00Z", + "detail": { + "changes": [ + { + "action": "created", + "after": "0.4", + "before": None, + "field": "session_recording_sample_rate", + "type": "Team", + }, + ], + "name": "Default project", + "short_id": None, + "trigger": None, + "type": None, + }, + "item_id": str(self.team.pk), + "scope": "Team", + "user": { + "email": "user1@posthog.com", + "first_name": "", + }, + }, + ] ) - self.assertEqual(response.status_code, 201) - batch_export = response.json() - batch_export_id = batch_export["id"] + @freeze_time("2022-02-08") + def test_team_creation_is_in_activity_log(self): + Team.objects.all().delete() + + self.organization_membership.level = OrganizationMembership.Level.ADMIN + self.organization_membership.save() + + team_name = str(uuid.uuid4()) + response = self.client.post("/api/environments/", {"name": team_name, "is_demo": False}) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + team_id = response.json()["id"] + self._assert_activity_log( + [ + { + "activity": "created", + "created_at": "2022-02-08T00:00:00Z", + "detail": { + "changes": None, + "name": team_name, + "short_id": None, + "trigger": None, + "type": None, + }, + "item_id": str(team_id), + "scope": "Team", + "user": { + "email": "user1@posthog.com", + "first_name": "", + }, + }, + ], + team_id=team_id, + ) - response = self.client.delete(f"/api/projects/{team.id}") - self.assertEqual(response.status_code, 204) + def test_team_is_cached_on_create_and_update(self): + Team.objects.all().delete() + self.organization_membership.level = OrganizationMembership.Level.ADMIN + self.organization_membership.save() + + response = self.client.post("/api/environments/", {"name": "Test", "is_demo": False}) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.json()["name"], "Test") + + token = response.json()["api_token"] + team_id = response.json()["id"] + + cached_team = get_team_in_cache(token) + + assert cached_team is not None + self.assertEqual(cached_team.name, "Test") + self.assertEqual(cached_team.uuid, response.json()["uuid"]) + self.assertEqual(cached_team.id, response.json()["id"]) + + response = self.client.patch( + f"/api/environments/{team_id}/", + {"timezone": "Europe/Istanbul", "session_recording_opt_in": True}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + cached_team = get_team_in_cache(token) + assert cached_team is not None - response = self.client.get(f"/api/projects/{team.id}/batch_exports/{batch_export_id}") - self.assertEqual(response.status_code, 404) + self.assertEqual(cached_team.name, "Test") + self.assertEqual(cached_team.uuid, response.json()["uuid"]) + self.assertEqual(cached_team.session_recording_opt_in, True) - with self.assertRaises(RPCError): - describe_schedule(temporal, batch_export_id) + # only things in CachedTeamSerializer are cached! + self.assertEqual(cached_team.timezone, "UTC") - @freeze_time("2022-02-08") - def test_reset_token(self): - self.organization_membership.level = OrganizationMembership.Level.ADMIN - self.organization_membership.save() + # reset token should update cache as well + response = self.client.patch(f"/api/environments/{team_id}/reset_token/") + response_data = response.json() - self._assert_activity_log_is_empty() + cached_team = get_team_in_cache(token) + assert cached_team is None - self.team.api_token = "xyz" - self.team.save() + cached_team = get_team_in_cache(response_data["api_token"]) + assert cached_team is not None + self.assertEqual(cached_team.name, "Test") + self.assertEqual(cached_team.uuid, response.json()["uuid"]) + self.assertEqual(cached_team.session_recording_opt_in, True) - response = self.client.patch(f"/api/projects/{self.team.id}/reset_token/") - response_data = response.json() + def test_turn_on_exception_autocapture(self): + response = self.client.get("/api/environments/@current/") + assert response.json()["autocapture_exceptions_opt_in"] is None + + response = self.client.patch( + "/api/environments/@current/", + {"autocapture_exceptions_opt_in": "Welwyn Garden City"}, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["detail"] == "Must be a valid boolean." + + response = self.client.patch("/api/environments/@current/", {"autocapture_exceptions_opt_in": True}) + assert response.status_code == status.HTTP_200_OK + response = self.client.get("/api/environments/@current/") + assert response.json()["autocapture_exceptions_opt_in"] is True + + def test_configure_exception_autocapture_event_dropping(self): + response = self.client.get("/api/environments/@current/") + assert response.json()["autocapture_exceptions_errors_to_ignore"] is None + + response = self.client.patch( + "/api/environments/@current/", + {"autocapture_exceptions_errors_to_ignore": {"wat": "am i"}}, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert ( + response.json()["detail"] == "Must provide a list for field: autocapture_exceptions_errors_to_ignore." + ) + + response = self.client.patch( + "/api/environments/@current/", + {"autocapture_exceptions_errors_to_ignore": [1, False]}, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert ( + response.json()["detail"] + == "Must provide a list of strings to field: autocapture_exceptions_errors_to_ignore." + ) + + response = self.client.patch( + "/api/environments/@current/", + {"autocapture_exceptions_errors_to_ignore": ["wat am i"]}, + ) + assert response.status_code == status.HTTP_200_OK + response = self.client.get("/api/environments/@current/") + assert response.json()["autocapture_exceptions_errors_to_ignore"] == ["wat am i"] + + def test_configure_exception_autocapture_event_dropping_only_allows_simple_config(self): + response = self.client.patch( + "/api/environments/@current/", + {"autocapture_exceptions_errors_to_ignore": ["abc" * 300]}, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert ( + response.json()["detail"] + == "Field autocapture_exceptions_errors_to_ignore must be less than 300 characters. Complex config should be provided in posthog-js initialization." + ) - self.team.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertNotEqual(response_data["api_token"], "xyz") - self.assertEqual(response_data["api_token"], self.team.api_token) - self.assertTrue(response_data["api_token"].startswith("phc_")) + @parameterized.expand( + [ + [ + "non numeric string", + "Welwyn Garden City", + "invalid_input", + "A valid number is required.", + ], + [ + "negative number", + "-1", + "min_value", + "Ensure this value is greater than or equal to 0.", + ], + [ + "greater than one", + "1.5", + "max_value", + "Ensure this value is less than or equal to 1.", + ], + [ + "too many digits", + "0.534", + "max_decimal_places", + "Ensure that there are no more than 2 decimal places.", + ], + ] + ) + def test_invalid_session_recording_sample_rates( + self, _name: str, provided_value: str, expected_code: str, expected_error: str + ) -> None: + response = self.client.patch( + "/api/environments/@current/", {"session_recording_sample_rate": provided_value} + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json() == { + "attr": "session_recording_sample_rate", + "code": expected_code, + "detail": expected_error, + "type": "validation_error", + } - self._assert_activity_log( + @parameterized.expand( [ - { - "activity": "updated", - "created_at": "2022-02-08T00:00:00Z", - "detail": { - "changes": [ - { - "action": "changed", - "after": None, - "before": None, - "field": "api_token", - "type": "Team", - }, - ], - "name": "Default project", - "short_id": None, - "trigger": None, - "type": None, - }, - "item_id": str(self.team.pk), - "scope": "Team", - "user": { - "email": "user1@posthog.com", - "first_name": "", - }, - }, + [ + "non numeric string", + "Trentham monkey forest", + "invalid_input", + "A valid integer is required.", + ], + [ + "negative number", + "-1", + "min_value", + "Ensure this value is greater than or equal to 0.", + ], + [ + "greater than 15000", + "15001", + "max_value", + "Ensure this value is less than or equal to 15000.", + ], + ["too many digits", "0.5", "invalid_input", "A valid integer is required."], ] ) + def test_invalid_session_recording_minimum_duration( + self, _name: str, provided_value: str, expected_code: str, expected_error: str + ) -> None: + response = self.client.patch( + "/api/environments/@current/", + {"session_recording_minimum_duration_milliseconds": provided_value}, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json() == { + "attr": "session_recording_minimum_duration_milliseconds", + "code": expected_code, + "detail": expected_error, + "type": "validation_error", + } - def test_reset_token_insufficient_priviledges(self): - self.team.api_token = "xyz" - self.team.save() - - response = self.client.patch(f"/api/projects/{self.team.id}/reset_token/") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_update_primary_dashboard(self): - d = Dashboard.objects.create(name="Test", team=self.team) - - # Can set it - response = self.client.patch("/api/projects/@current/", {"primary_dashboard": d.id}) - response_data = response.json() - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response_data["name"], self.team.name) - self.assertEqual(response_data["primary_dashboard"], d.id) - - def test_cant_set_primary_dashboard_to_another_teams_dashboard(self): - team_2 = Team.objects.create(organization=self.organization, name="Default project") - d = Dashboard.objects.create(name="Test", team=team_2) - - response = self.client.patch("/api/projects/@current/", {"primary_dashboard": d.id}) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - response = self.client.get("/api/projects/@current/") - response_data = response.json() - self.assertEqual(response_data["primary_dashboard"], None) - - def test_is_generating_demo_data(self): - cache_key = f"is_generating_demo_data_{self.team.pk}" - cache.set(cache_key, "True") - response = self.client.get(f"/api/projects/{self.team.id}/is_generating_demo_data/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json(), {"is_generating_demo_data": True}) - cache.delete(cache_key) - response = self.client.get(f"/api/projects/{self.team.id}/is_generating_demo_data/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json(), {"is_generating_demo_data": False}) - - @patch("posthog.api.team.create_data_for_demo_team.delay") - def test_org_member_can_create_demo_project(self, mock_create_data_for_demo_team: MagicMock): - self.organization_membership.level = OrganizationMembership.Level.MEMBER - self.organization_membership.save() - response = self.client.post("/api/projects/", {"name": "Hedgebox", "is_demo": True}) - mock_create_data_for_demo_team.assert_called_once() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - @freeze_time("2022-02-08") - def test_team_float_config_can_be_serialized_to_activity_log(self): - # regression test since this isn't true by default - response = self.client.patch(f"/api/projects/@current/", {"session_recording_sample_rate": 0.4}) - assert response.status_code == status.HTTP_200_OK - self._assert_activity_log( + @parameterized.expand( [ - { - "activity": "updated", - "created_at": "2022-02-08T00:00:00Z", - "detail": { - "changes": [ - { - "action": "created", - "after": "0.4", - "before": None, - "field": "session_recording_sample_rate", - "type": "Team", - }, - ], - "name": "Default project", - "short_id": None, - "trigger": None, - "type": None, - }, - "item_id": str(self.team.pk), - "scope": "Team", - "user": { - "email": "user1@posthog.com", - "first_name": "", - }, - }, + [ + "string", + "Marple bridge", + "invalid_input", + "Must provide a dictionary or None.", + ], + ["numeric string", "-1", "invalid_input", "Must provide a dictionary or None."], + ["numeric", 1, "invalid_input", "Must provide a dictionary or None."], + ["numeric positive string", "1", "invalid_input", "Must provide a dictionary or None."], + [ + "unexpected json - no id", + {"key": "something"}, + "invalid_input", + "Must provide a dictionary with only 'id' and 'key' keys. _or_ only 'id', 'key', and 'variant' keys.", + ], + [ + "unexpected json - no key", + {"id": 1}, + "invalid_input", + "Must provide a dictionary with only 'id' and 'key' keys. _or_ only 'id', 'key', and 'variant' keys.", + ], + [ + "unexpected json - only variant", + {"variant": "1"}, + "invalid_input", + "Must provide a dictionary with only 'id' and 'key' keys. _or_ only 'id', 'key', and 'variant' keys.", + ], + [ + "unexpected json - variant must be string", + {"variant": 1}, + "invalid_input", + "Must provide a dictionary with only 'id' and 'key' keys. _or_ only 'id', 'key', and 'variant' keys.", + ], + [ + "unexpected json - missing id", + {"key": "one", "variant": "1"}, + "invalid_input", + "Must provide a dictionary with only 'id' and 'key' keys. _or_ only 'id', 'key', and 'variant' keys.", + ], + [ + "unexpected json - missing key", + {"id": "one", "variant": "1"}, + "invalid_input", + "Must provide a dictionary with only 'id' and 'key' keys. _or_ only 'id', 'key', and 'variant' keys.", + ], + [ + "unexpected json - neither", + {"wat": "wat"}, + "invalid_input", + "Must provide a dictionary with only 'id' and 'key' keys. _or_ only 'id', 'key', and 'variant' keys.", + ], ] ) + def test_invalid_session_recording_linked_flag( + self, _name: str, provided_value: Any, expected_code: str, expected_error: str + ) -> None: + response = self._patch_linked_flag_config(provided_value, expected_status=status.HTTP_400_BAD_REQUEST) + + assert response.json() == { + "attr": "session_recording_linked_flag", + "code": expected_code, + "detail": expected_error, + "type": "validation_error", + } + + def test_can_set_and_unset_session_recording_linked_flag(self) -> None: + self._patch_linked_flag_config({"id": 1, "key": "provided_value"}) + self._assert_linked_flag_config({"id": 1, "key": "provided_value"}) - @freeze_time("2022-02-08") - def test_team_creation_is_in_activity_log(self): - Team.objects.all().delete() + self._patch_linked_flag_config(None) + self._assert_linked_flag_config(None) - self.organization_membership.level = OrganizationMembership.Level.ADMIN - self.organization_membership.save() + def test_can_set_and_unset_session_recording_linked_flag_variant(self) -> None: + self._patch_linked_flag_config({"id": 1, "key": "provided_value", "variant": "test"}) + self._assert_linked_flag_config({"id": 1, "key": "provided_value", "variant": "test"}) - team_name = str(uuid.uuid4()) - response = self.client.post("/api/projects/", {"name": team_name, "is_demo": False}) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self._patch_linked_flag_config(None) + self._assert_linked_flag_config(None) - team_id = response.json()["id"] - self._assert_activity_log( + @parameterized.expand( [ - { - "activity": "created", - "created_at": "2022-02-08T00:00:00Z", - "detail": { - "changes": None, - "name": team_name, - "short_id": None, - "trigger": None, - "type": None, - }, - "item_id": str(team_id), - "scope": "Team", - "user": { - "email": "user1@posthog.com", - "first_name": "", - }, - }, - ], - team_id=team_id, + [ + "string", + "Marple bridge", + "invalid_input", + "Must provide a dictionary or None.", + ], + ["numeric", "-1", "invalid_input", "Must provide a dictionary or None."], + [ + "unexpected json - no recordX", + {"key": "something"}, + "invalid_input", + "Must provide a dictionary with only 'recordHeaders' and/or 'recordBody' keys.", + ], + ] ) + def test_invalid_session_recording_network_payload_capture_config( + self, _name: str, provided_value: str, expected_code: str, expected_error: str + ) -> None: + response = self.client.patch( + "/api/environments/@current/", {"session_recording_network_payload_capture_config": provided_value} + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json() == { + "attr": "session_recording_network_payload_capture_config", + "code": expected_code, + "detail": expected_error, + "type": "validation_error", + } - def test_team_is_cached_on_create_and_update(self): - Team.objects.all().delete() - self.organization_membership.level = OrganizationMembership.Level.ADMIN - self.organization_membership.save() - - response = self.client.post("/api/projects/", {"name": "Test", "is_demo": False}) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response.json()["name"], "Test") + def test_can_set_and_unset_session_recording_network_payload_capture_config(self) -> None: + # can set just one + first_patch_response = self.client.patch( + "/api/environments/@current/", + {"session_recording_network_payload_capture_config": {"recordHeaders": True}}, + ) + assert first_patch_response.status_code == status.HTTP_200_OK + get_response = self.client.get("/api/environments/@current/") + assert get_response.json()["session_recording_network_payload_capture_config"] == {"recordHeaders": True} + + # can set the other + first_patch_response = self.client.patch( + "/api/environments/@current/", + {"session_recording_network_payload_capture_config": {"recordBody": False}}, + ) + assert first_patch_response.status_code == status.HTTP_200_OK + get_response = self.client.get("/api/environments/@current/") + assert get_response.json()["session_recording_network_payload_capture_config"] == {"recordBody": False} - token = response.json()["api_token"] - team_id = response.json()["id"] + # can unset both + response = self.client.patch( + "/api/environments/@current/", {"session_recording_network_payload_capture_config": None} + ) + assert response.status_code == status.HTTP_200_OK + second_get_response = self.client.get("/api/environments/@current/") + assert second_get_response.json()["session_recording_network_payload_capture_config"] is None - cached_team = get_team_in_cache(token) + def test_can_set_and_unset_session_replay_config(self) -> None: + # can set + self._patch_session_replay_config({"record_canvas": True}) + self._assert_replay_config_is({"record_canvas": True}) - assert cached_team is not None - self.assertEqual(cached_team.name, "Test") - self.assertEqual(cached_team.uuid, response.json()["uuid"]) - self.assertEqual(cached_team.id, response.json()["id"]) + # can unset + self._patch_session_replay_config(None) + self._assert_replay_config_is(None) - response = self.client.patch( - f"/api/projects/{team_id}/", - {"timezone": "Europe/Istanbul", "session_recording_opt_in": True}, + @parameterized.expand( + [ + [ + "string", + "Marple bridge", + "invalid_input", + "Must provide a dictionary or None.", + ], + ["numeric", "-1", "invalid_input", "Must provide a dictionary or None."], + [ + "unexpected json - no record", + {"key": "something"}, + "invalid_input", + "Must provide a dictionary with only allowed keys: included_event_properties, opt_in, preferred_events, excluded_events, important_user_properties.", + ], + ] ) - self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_invalid_session_replay_config_ai_config( + self, _name: str, provided_value: str, expected_code: str, expected_error: str + ) -> None: + response = self._patch_session_replay_config( + {"ai_config": provided_value}, expected_status=status.HTTP_400_BAD_REQUEST + ) + assert response.json() == { + "attr": "session_replay_config", + "code": expected_code, + "detail": expected_error, + "type": "validation_error", + } - cached_team = get_team_in_cache(token) - assert cached_team is not None + def test_can_set_and_unset_session_replay_config_ai_config(self) -> None: + # can set just the opt-in + self._patch_session_replay_config({"ai_config": {"opt_in": True}}) + self._assert_replay_config_is({"ai_config": {"opt_in": True}}) - self.assertEqual(cached_team.name, "Test") - self.assertEqual(cached_team.uuid, response.json()["uuid"]) - self.assertEqual(cached_team.session_recording_opt_in, True) + # can set some preferences + self._patch_session_replay_config( + {"ai_config": {"opt_in": False, "included_event_properties": ["something"]}} + ) + self._assert_replay_config_is({"ai_config": {"opt_in": False, "included_event_properties": ["something"]}}) - # only things in CachedTeamSerializer are cached! - self.assertEqual(cached_team.timezone, "UTC") + self._patch_session_replay_config({"ai_config": None}) + self._assert_replay_config_is({"ai_config": None}) - # reset token should update cache as well - response = self.client.patch(f"/api/projects/{team_id}/reset_token/") - response_data = response.json() + def test_can_set_replay_configs_without_providing_them_all(self) -> None: + # can set just the opt-in + self._patch_session_replay_config({"ai_config": {"opt_in": True}}) + self._assert_replay_config_is({"ai_config": {"opt_in": True}}) - cached_team = get_team_in_cache(token) - assert cached_team is None + self._patch_session_replay_config({"record_canvas": True}) + self._assert_replay_config_is({"record_canvas": True, "ai_config": {"opt_in": True}}) - cached_team = get_team_in_cache(response_data["api_token"]) - assert cached_team is not None - self.assertEqual(cached_team.name, "Test") - self.assertEqual(cached_team.uuid, response.json()["uuid"]) - self.assertEqual(cached_team.session_recording_opt_in, True) + def test_can_set_replay_configs_without_providing_them_all_even_when_either_side_is_none(self) -> None: + # because we do some dictionary copying we need a regression test to ensure we can always set and unset keys + self._patch_session_replay_config({"record_canvas": True, "ai_config": {"opt_in": True}}) + self._assert_replay_config_is({"record_canvas": True, "ai_config": {"opt_in": True}}) - def test_turn_on_exception_autocapture(self): - response = self.client.get("/api/projects/@current/") - assert response.json()["autocapture_exceptions_opt_in"] is None + self._patch_session_replay_config({"record_canvas": None}) + self._assert_replay_config_is({"record_canvas": None, "ai_config": {"opt_in": True}}) - response = self.client.patch( - "/api/projects/@current/", - {"autocapture_exceptions_opt_in": "Welwyn Garden City"}, - ) - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json()["detail"] == "Must be a valid boolean." + # top-level from having a value to None + self._patch_session_replay_config(None) + self._assert_replay_config_is(None) - response = self.client.patch("/api/projects/@current/", {"autocapture_exceptions_opt_in": True}) - assert response.status_code == status.HTTP_200_OK - response = self.client.get("/api/projects/@current/") - assert response.json()["autocapture_exceptions_opt_in"] is True + # top-level from None to having a value + self._patch_session_replay_config({"ai_config": None}) + self._assert_replay_config_is({"ai_config": None}) - def test_configure_exception_autocapture_event_dropping(self): - response = self.client.get("/api/projects/@current/") - assert response.json()["autocapture_exceptions_errors_to_ignore"] is None + # next-level from None to having a value + self._patch_session_replay_config({"ai_config": {"opt_in": True}}) + self._assert_replay_config_is({"ai_config": {"opt_in": True}}) - response = self.client.patch( - "/api/projects/@current/", - {"autocapture_exceptions_errors_to_ignore": {"wat": "am i"}}, - ) - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json()["detail"] == "Must provide a list for field: autocapture_exceptions_errors_to_ignore." + # next-level from having a value to None + self._patch_session_replay_config({"ai_config": None}) + self._assert_replay_config_is({"ai_config": None}) - response = self.client.patch( - "/api/projects/@current/", - {"autocapture_exceptions_errors_to_ignore": [1, False]}, - ) - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert ( - response.json()["detail"] - == "Must provide a list of strings to field: autocapture_exceptions_errors_to_ignore." - ) + def test_can_set_replay_configs_patch_session_replay_config_one_level_deep(self) -> None: + # can set just the opt-in + self._patch_session_replay_config({"ai_config": {"opt_in": True}}) + self._assert_replay_config_is({"ai_config": {"opt_in": True}}) - response = self.client.patch( - "/api/projects/@current/", - {"autocapture_exceptions_errors_to_ignore": ["wat am i"]}, - ) - assert response.status_code == status.HTTP_200_OK - response = self.client.get("/api/projects/@current/") - assert response.json()["autocapture_exceptions_errors_to_ignore"] == ["wat am i"] - - def test_configure_exception_autocapture_event_dropping_only_allows_simple_config(self): - response = self.client.patch( - "/api/projects/@current/", - {"autocapture_exceptions_errors_to_ignore": ["abc" * 300]}, - ) - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert ( - response.json()["detail"] - == "Field autocapture_exceptions_errors_to_ignore must be less than 300 characters. Complex config should be provided in posthog-js initialization." - ) + self._patch_session_replay_config({"ai_config": {"included_event_properties": ["something"]}}) + # even though opt_in was not provided in the patch it should be preserved + self._assert_replay_config_is({"ai_config": {"opt_in": True, "included_event_properties": ["something"]}}) - @parameterized.expand( - [ - [ - "non numeric string", - "Welwyn Garden City", - "invalid_input", - "A valid number is required.", - ], - [ - "negative number", - "-1", - "min_value", - "Ensure this value is greater than or equal to 0.", - ], - [ - "greater than one", - "1.5", - "max_value", - "Ensure this value is less than or equal to 1.", - ], - [ - "too many digits", - "0.534", - "max_decimal_places", - "Ensure that there are no more than 2 decimal places.", - ], - ] - ) - def test_invalid_session_recording_sample_rates( - self, _name: str, provided_value: str, expected_code: str, expected_error: str - ) -> None: - response = self.client.patch("/api/projects/@current/", {"session_recording_sample_rate": provided_value}) - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json() == { - "attr": "session_recording_sample_rate", - "code": expected_code, - "detail": expected_error, - "type": "validation_error", - } - - @parameterized.expand( - [ - [ - "non numeric string", - "Trentham monkey forest", - "invalid_input", - "A valid integer is required.", - ], - [ - "negative number", - "-1", - "min_value", - "Ensure this value is greater than or equal to 0.", - ], - [ - "greater than 15000", - "15001", - "max_value", - "Ensure this value is less than or equal to 15000.", - ], - ["too many digits", "0.5", "invalid_input", "A valid integer is required."], - ] - ) - def test_invalid_session_recording_minimum_duration( - self, _name: str, provided_value: str, expected_code: str, expected_error: str - ) -> None: - response = self.client.patch( - "/api/projects/@current/", - {"session_recording_minimum_duration_milliseconds": provided_value}, - ) - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json() == { - "attr": "session_recording_minimum_duration_milliseconds", - "code": expected_code, - "detail": expected_error, - "type": "validation_error", - } - - @parameterized.expand( - [ - [ - "string", - "Marple bridge", - "invalid_input", - "Must provide a dictionary or None.", - ], - ["numeric string", "-1", "invalid_input", "Must provide a dictionary or None."], - ["numeric", 1, "invalid_input", "Must provide a dictionary or None."], - ["numeric positive string", "1", "invalid_input", "Must provide a dictionary or None."], - [ - "unexpected json - no id", - {"key": "something"}, - "invalid_input", - "Must provide a dictionary with only 'id' and 'key' keys. _or_ only 'id', 'key', and 'variant' keys.", - ], - [ - "unexpected json - no key", - {"id": 1}, - "invalid_input", - "Must provide a dictionary with only 'id' and 'key' keys. _or_ only 'id', 'key', and 'variant' keys.", - ], - [ - "unexpected json - only variant", - {"variant": "1"}, - "invalid_input", - "Must provide a dictionary with only 'id' and 'key' keys. _or_ only 'id', 'key', and 'variant' keys.", - ], - [ - "unexpected json - variant must be string", - {"variant": 1}, - "invalid_input", - "Must provide a dictionary with only 'id' and 'key' keys. _or_ only 'id', 'key', and 'variant' keys.", - ], - [ - "unexpected json - missing id", - {"key": "one", "variant": "1"}, - "invalid_input", - "Must provide a dictionary with only 'id' and 'key' keys. _or_ only 'id', 'key', and 'variant' keys.", - ], - [ - "unexpected json - missing key", - {"id": "one", "variant": "1"}, - "invalid_input", - "Must provide a dictionary with only 'id' and 'key' keys. _or_ only 'id', 'key', and 'variant' keys.", - ], - [ - "unexpected json - neither", - {"wat": "wat"}, - "invalid_input", - "Must provide a dictionary with only 'id' and 'key' keys. _or_ only 'id', 'key', and 'variant' keys.", - ], - ] - ) - def test_invalid_session_recording_linked_flag( - self, _name: str, provided_value: Any, expected_code: str, expected_error: str - ) -> None: - response = self._patch_linked_flag_config(provided_value, expected_status=status.HTTP_400_BAD_REQUEST) - - assert response.json() == { - "attr": "session_recording_linked_flag", - "code": expected_code, - "detail": expected_error, - "type": "validation_error", - } - - def test_can_set_and_unset_session_recording_linked_flag(self) -> None: - self._patch_linked_flag_config({"id": 1, "key": "provided_value"}) - self._assert_linked_flag_config({"id": 1, "key": "provided_value"}) - - self._patch_linked_flag_config(None) - self._assert_linked_flag_config(None) - - def test_can_set_and_unset_session_recording_linked_flag_variant(self) -> None: - self._patch_linked_flag_config({"id": 1, "key": "provided_value", "variant": "test"}) - self._assert_linked_flag_config({"id": 1, "key": "provided_value", "variant": "test"}) - - self._patch_linked_flag_config(None) - self._assert_linked_flag_config(None) - - @parameterized.expand( - [ - [ - "string", - "Marple bridge", - "invalid_input", - "Must provide a dictionary or None.", - ], - ["numeric", "-1", "invalid_input", "Must provide a dictionary or None."], - [ - "unexpected json - no recordX", - {"key": "something"}, - "invalid_input", - "Must provide a dictionary with only 'recordHeaders' and/or 'recordBody' keys.", - ], - ] - ) - def test_invalid_session_recording_network_payload_capture_config( - self, _name: str, provided_value: str, expected_code: str, expected_error: str - ) -> None: - response = self.client.patch( - "/api/projects/@current/", {"session_recording_network_payload_capture_config": provided_value} - ) - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json() == { - "attr": "session_recording_network_payload_capture_config", - "code": expected_code, - "detail": expected_error, - "type": "validation_error", - } - - def test_can_set_and_unset_session_recording_network_payload_capture_config(self) -> None: - # can set just one - first_patch_response = self.client.patch( - "/api/projects/@current/", - {"session_recording_network_payload_capture_config": {"recordHeaders": True}}, - ) - assert first_patch_response.status_code == status.HTTP_200_OK - get_response = self.client.get("/api/projects/@current/") - assert get_response.json()["session_recording_network_payload_capture_config"] == {"recordHeaders": True} - - # can set the other - first_patch_response = self.client.patch( - "/api/projects/@current/", - {"session_recording_network_payload_capture_config": {"recordBody": False}}, - ) - assert first_patch_response.status_code == status.HTTP_200_OK - get_response = self.client.get("/api/projects/@current/") - assert get_response.json()["session_recording_network_payload_capture_config"] == {"recordBody": False} + self._patch_session_replay_config( + {"ai_config": {"opt_in": None, "included_event_properties": ["something"]}} + ) + # even though opt_in was not provided in the patch it should be preserved + self._assert_replay_config_is({"ai_config": {"opt_in": None, "included_event_properties": ["something"]}}) + + # but we don't go into the next nested level and patch that data + # sending a new value without the original + self._patch_session_replay_config({"ai_config": {"included_event_properties": ["and another"]}}) + # and the existing second level nesting is not preserved + self._assert_replay_config_is({"ai_config": {"opt_in": None, "included_event_properties": ["and another"]}}) + + def _assert_replay_config_is(self, expected: dict[str, Any] | None) -> HttpResponse: + get_response = self.client.get("/api/environments/@current/") + assert get_response.status_code == status.HTTP_200_OK, get_response.json() + assert get_response.json()["session_replay_config"] == expected + + return get_response + + def _patch_session_replay_config( + self, config: dict[str, Any] | None, expected_status: int = status.HTTP_200_OK + ) -> HttpResponse: + patch_response = self.client.patch( + "/api/environments/@current/", + {"session_replay_config": config}, + ) + assert patch_response.status_code == expected_status, patch_response.json() - # can unset both - response = self.client.patch( - "/api/projects/@current/", {"session_recording_network_payload_capture_config": None} - ) - assert response.status_code == status.HTTP_200_OK - second_get_response = self.client.get("/api/projects/@current/") - assert second_get_response.json()["session_recording_network_payload_capture_config"] is None + return patch_response - def test_can_set_and_unset_session_replay_config(self) -> None: - # can set - self._patch_session_replay_config({"record_canvas": True}) - self._assert_replay_config_is({"record_canvas": True}) + def _assert_linked_flag_config(self, expected_config: dict | None) -> HttpResponse: + response = self.client.get("/api/environments/@current/") + assert response.status_code == status.HTTP_200_OK + assert response.json()["session_recording_linked_flag"] == expected_config + return response - # can unset - self._patch_session_replay_config(None) - self._assert_replay_config_is(None) + def _patch_linked_flag_config( + self, config: dict | None, expected_status: int = status.HTTP_200_OK + ) -> HttpResponse: + response = self.client.patch("/api/environments/@current/", {"session_recording_linked_flag": config}) + assert response.status_code == expected_status, response.json() + return response - @parameterized.expand( - [ - [ - "string", - "Marple bridge", - "invalid_input", - "Must provide a dictionary or None.", - ], - ["numeric", "-1", "invalid_input", "Must provide a dictionary or None."], - [ - "unexpected json - no record", - {"key": "something"}, - "invalid_input", - "Must provide a dictionary with only allowed keys: included_event_properties, opt_in, preferred_events, excluded_events, important_user_properties.", - ], - ] - ) - def test_invalid_session_replay_config_ai_config( - self, _name: str, provided_value: str, expected_code: str, expected_error: str - ) -> None: - response = self._patch_session_replay_config( - {"ai_config": provided_value}, expected_status=status.HTTP_400_BAD_REQUEST - ) - assert response.json() == { - "attr": "session_replay_config", - "code": expected_code, - "detail": expected_error, - "type": "validation_error", - } - - def test_can_set_and_unset_session_replay_config_ai_config(self) -> None: - # can set just the opt-in - self._patch_session_replay_config({"ai_config": {"opt_in": True}}) - self._assert_replay_config_is({"ai_config": {"opt_in": True}}) - - # can set some preferences - self._patch_session_replay_config({"ai_config": {"opt_in": False, "included_event_properties": ["something"]}}) - self._assert_replay_config_is({"ai_config": {"opt_in": False, "included_event_properties": ["something"]}}) - - self._patch_session_replay_config({"ai_config": None}) - self._assert_replay_config_is({"ai_config": None}) - - def test_can_set_replay_configs_without_providing_them_all(self) -> None: - # can set just the opt-in - self._patch_session_replay_config({"ai_config": {"opt_in": True}}) - self._assert_replay_config_is({"ai_config": {"opt_in": True}}) - - self._patch_session_replay_config({"record_canvas": True}) - self._assert_replay_config_is({"record_canvas": True, "ai_config": {"opt_in": True}}) - - def test_can_set_replay_configs_without_providing_them_all_even_when_either_side_is_none(self) -> None: - # because we do some dictionary copying we need a regression test to ensure we can always set and unset keys - self._patch_session_replay_config({"record_canvas": True, "ai_config": {"opt_in": True}}) - self._assert_replay_config_is({"record_canvas": True, "ai_config": {"opt_in": True}}) - - self._patch_session_replay_config({"record_canvas": None}) - self._assert_replay_config_is({"record_canvas": None, "ai_config": {"opt_in": True}}) - - # top-level from having a value to None - self._patch_session_replay_config(None) - self._assert_replay_config_is(None) - - # top-level from None to having a value - self._patch_session_replay_config({"ai_config": None}) - self._assert_replay_config_is({"ai_config": None}) - - # next-level from None to having a value - self._patch_session_replay_config({"ai_config": {"opt_in": True}}) - self._assert_replay_config_is({"ai_config": {"opt_in": True}}) - - # next-level from having a value to None - self._patch_session_replay_config({"ai_config": None}) - self._assert_replay_config_is({"ai_config": None}) - - def test_can_set_replay_configs_patch_session_replay_config_one_level_deep(self) -> None: - # can set just the opt-in - self._patch_session_replay_config({"ai_config": {"opt_in": True}}) - self._assert_replay_config_is({"ai_config": {"opt_in": True}}) - - self._patch_session_replay_config({"ai_config": {"included_event_properties": ["something"]}}) - # even though opt_in was not provided in the patch it should be preserved - self._assert_replay_config_is({"ai_config": {"opt_in": True, "included_event_properties": ["something"]}}) - - self._patch_session_replay_config({"ai_config": {"opt_in": None, "included_event_properties": ["something"]}}) - # even though opt_in was not provided in the patch it should be preserved - self._assert_replay_config_is({"ai_config": {"opt_in": None, "included_event_properties": ["something"]}}) - - # but we don't go into the next nested level and patch that data - # sending a new value without the original - self._patch_session_replay_config({"ai_config": {"included_event_properties": ["and another"]}}) - # and the existing second level nesting is not preserved - self._assert_replay_config_is({"ai_config": {"opt_in": None, "included_event_properties": ["and another"]}}) - - def _assert_replay_config_is(self, expected: dict[str, Any] | None) -> HttpResponse: - get_response = self.client.get("/api/projects/@current/") - assert get_response.status_code == status.HTTP_200_OK, get_response.json() - assert get_response.json()["session_replay_config"] == expected - - return get_response - - def _patch_session_replay_config( - self, config: dict[str, Any] | None, expected_status: int = status.HTTP_200_OK - ) -> HttpResponse: - patch_response = self.client.patch( - "/api/projects/@current/", - {"session_replay_config": config}, - ) - assert patch_response.status_code == expected_status, patch_response.json() + return TestTeamAPI - return patch_response - def _assert_linked_flag_config(self, expected_config: dict | None) -> HttpResponse: - response = self.client.get("/api/projects/@current/") - assert response.status_code == status.HTTP_200_OK - assert response.json()["session_recording_linked_flag"] == expected_config - return response +class EnvironmentToProjectRewriteClient(test.APIClient): + """ + This client rewrites all requests to the /api/environments/ endpoint ("proper" environments endpoint) + to /api/projects/ (previously known as the "team" endpoint). Allows us to test for backwards compatibility of + the /api/projects/ endpoint - for use in `test_project.py`. + """ - def _patch_linked_flag_config(self, config: dict | None, expected_status: int = status.HTTP_200_OK) -> HttpResponse: - response = self.client.patch("/api/projects/@current/", {"session_recording_linked_flag": config}) - assert response.status_code == expected_status, response.json() - return response + def generic( + self, + method, + path, + data="", + content_type="application/octet-stream", + secure=False, + *, + headers=None, + **extra, + ): + path = path.replace("/api/environments/", "/api/projects/") + return super().generic(method, path, data, content_type, secure, headers=headers, **extra) def create_team(organization: Organization, name: str = "Test team", timezone: str = "UTC") -> Team: @@ -1059,10 +1148,5 @@ def create_team(organization: Organization, name: str = "Test team", timezone: s ) -async def acreate_team(organization: Organization, name: str = "Test team", timezone: str = "UTC") -> Team: - """ - This is a helper that just creates a team. It currently uses the orm, but we - could use either the api, or django admin to create, to get better parity - with real world scenarios. - """ - return await sync_to_async(create_team)(organization, name=name, timezone=timezone) +class TestTeamAPI(team_api_test_factory()): # type: ignore + pass diff --git a/posthog/demo/legacy/__init__.py b/posthog/demo/legacy/__init__.py index a62ca3f350e23..194eb3fcc0542 100644 --- a/posthog/demo/legacy/__init__.py +++ b/posthog/demo/legacy/__init__.py @@ -19,21 +19,6 @@ def demo_route(request: HttpRequest): return render_template("demo.html", request=request, context={"api_token": project_api_token}) -def create_demo_team(organization: Organization, *args) -> Team: - team = Team.objects.create_with_data( - default_dashboards=False, - organization=organization, - name=TEAM_NAME, - ingested_event=True, - completed_snippet_onboarding=True, - session_recording_opt_in=True, - is_demo=True, - ) - create_demo_data(team) - EventDefinition.objects.get_or_create(team=team, name="$pageview") - return team - - def create_demo_data(team: Team, dashboards=True): WebDataGenerator(team, n_people=40).create(dashboards=dashboards) AppDataGenerator(team, n_people=100).create(dashboards=dashboards) diff --git a/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr b/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr index 69d6e856f5540..2fe83c79dc067 100644 --- a/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr +++ b/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr @@ -840,6 +840,18 @@ ''' # --- # name: TestTrends.test_dau_with_breakdown_filtering_with_sampling + ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.1 ''' SELECT groupArray(1)(date)[1] AS date, arrayFold((acc, x) -> arrayMap(i -> plus(acc[i], x[i]), range(1, plus(length(date), 1))), groupArray(total), arrayWithConstant(length(date), reinterpretAsFloat64(0))) AS total, @@ -886,7 +898,31 @@ max_bytes_before_external_group_by=0 ''' # --- -# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.1 +# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.10 + ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.11 + ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.12 ''' SELECT groupArray(1)(date)[1] AS date, arrayFold((acc, x) -> arrayMap(i -> plus(acc[i], x[i]), range(1, plus(length(date), 1))), groupArray(total), arrayWithConstant(length(date), reinterpretAsFloat64(0))) AS total, @@ -933,7 +969,54 @@ max_bytes_before_external_group_by=0 ''' # --- -# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.2 +# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.13 + ''' + SELECT groupArray(1)(date)[1] AS date, + arrayFold((acc, x) -> arrayMap(i -> plus(acc[i], x[i]), range(1, plus(length(date), 1))), groupArray(total), arrayWithConstant(length(date), reinterpretAsFloat64(0))) AS total, + if(ifNull(ifNull(greaterOrEquals(row_number, 25), 0), 0), '$$_posthog_breakdown_other_$$', breakdown_value) AS breakdown_value + FROM + (SELECT arrayMap(number -> plus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))))), 1))) AS date, + arrayMap(_match_date -> arraySum(arraySlice(groupArray(count), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> ifNull(equals(x, _match_date), isNull(x) + and isNull(_match_date)), _days_for_count), _index), 1))), date) AS total, + breakdown_value AS breakdown_value, + rowNumberInAllBlocks() AS row_number + FROM + (SELECT sum(total) AS count, + day_start AS day_start, + breakdown_value AS breakdown_value + FROM + (SELECT count(DISTINCT e__pdi.person_id) AS total, + toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, + ifNull(nullIf(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), ''), '$$_posthog_breakdown_null_$$') AS breakdown_value + FROM events AS e SAMPLE 1.0 + INNER JOIN + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + person_distinct_id2.distinct_id AS distinct_id + FROM person_distinct_id2 + WHERE equals(person_distinct_id2.team_id, 2) + GROUP BY person_distinct_id2.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up')) + GROUP BY day_start, + breakdown_value) + GROUP BY day_start, + breakdown_value + ORDER BY day_start ASC, breakdown_value ASC) + GROUP BY breakdown_value + ORDER BY if(ifNull(equals(breakdown_value, '$$_posthog_breakdown_other_$$'), 0), 2, if(ifNull(equals(breakdown_value, '$$_posthog_breakdown_null_$$'), 0), 1, 0)) ASC, arraySum(total) DESC, breakdown_value ASC) + WHERE isNotNull(breakdown_value) + GROUP BY breakdown_value + ORDER BY if(ifNull(equals(breakdown_value, '$$_posthog_breakdown_other_$$'), 0), 2, if(ifNull(equals(breakdown_value, '$$_posthog_breakdown_null_$$'), 0), 1, 0)) ASC, arraySum(total) DESC, breakdown_value ASC + LIMIT 50000 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1, + format_csv_allow_double_quotes=0, + max_ast_elements=4000000, + max_expanded_ast_elements=4000000, + max_bytes_before_external_group_by=0 + ''' +# --- +# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.14 ''' SELECT groupArray(1)(date)[1] AS date, arrayFold((acc, x) -> arrayMap(i -> plus(acc[i], x[i]), range(1, plus(length(date), 1))), groupArray(total), arrayWithConstant(length(date), reinterpretAsFloat64(0))) AS total, @@ -980,6 +1063,100 @@ max_bytes_before_external_group_by=0 ''' # --- +# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.15 + ''' + SELECT groupArray(1)(date)[1] AS date, + arrayFold((acc, x) -> arrayMap(i -> plus(acc[i], x[i]), range(1, plus(length(date), 1))), groupArray(total), arrayWithConstant(length(date), reinterpretAsFloat64(0))) AS total, + arrayMap(i -> if(ifNull(ifNull(greaterOrEquals(row_number, 25), 0), 0), '$$_posthog_breakdown_other_$$', i), breakdown_value) AS breakdown_value + FROM + (SELECT arrayMap(number -> plus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))))), 1))) AS date, + arrayMap(_match_date -> arraySum(arraySlice(groupArray(count), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> ifNull(equals(x, _match_date), isNull(x) + and isNull(_match_date)), _days_for_count), _index), 1))), date) AS total, + breakdown_value AS breakdown_value, + rowNumberInAllBlocks() AS row_number + FROM + (SELECT sum(total) AS count, + day_start AS day_start, + [ifNull(toString(breakdown_value_1), '$$_posthog_breakdown_null_$$')] AS breakdown_value + FROM + (SELECT count(DISTINCT e__pdi.person_id) AS total, + toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, + ifNull(nullIf(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), ''), '$$_posthog_breakdown_null_$$') AS breakdown_value_1 + FROM events AS e SAMPLE 1.0 + INNER JOIN + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + person_distinct_id2.distinct_id AS distinct_id + FROM person_distinct_id2 + WHERE equals(person_distinct_id2.team_id, 2) + GROUP BY person_distinct_id2.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up')) + GROUP BY day_start, + breakdown_value_1) + GROUP BY day_start, + breakdown_value_1 + ORDER BY day_start ASC, breakdown_value ASC) + GROUP BY breakdown_value + ORDER BY if(has(breakdown_value, '$$_posthog_breakdown_other_$$'), 2, if(has(breakdown_value, '$$_posthog_breakdown_null_$$'), 1, 0)) ASC, arraySum(total) DESC, breakdown_value ASC) + WHERE arrayExists(x -> isNotNull(x), breakdown_value) + GROUP BY breakdown_value + ORDER BY if(has(breakdown_value, '$$_posthog_breakdown_other_$$'), 2, if(has(breakdown_value, '$$_posthog_breakdown_null_$$'), 1, 0)) ASC, arraySum(total) DESC, breakdown_value ASC + LIMIT 50000 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1, + format_csv_allow_double_quotes=0, + max_ast_elements=4000000, + max_expanded_ast_elements=4000000, + max_bytes_before_external_group_by=0 + ''' +# --- +# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.2 + ''' + SELECT groupArray(1)(date)[1] AS date, + arrayFold((acc, x) -> arrayMap(i -> plus(acc[i], x[i]), range(1, plus(length(date), 1))), groupArray(total), arrayWithConstant(length(date), reinterpretAsFloat64(0))) AS total, + if(ifNull(ifNull(greaterOrEquals(row_number, 25), 0), 0), '$$_posthog_breakdown_other_$$', breakdown_value) AS breakdown_value + FROM + (SELECT arrayMap(number -> plus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))))), 1))) AS date, + arrayMap(_match_date -> arraySum(arraySlice(groupArray(count), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> ifNull(equals(x, _match_date), isNull(x) + and isNull(_match_date)), _days_for_count), _index), 1))), date) AS total, + breakdown_value AS breakdown_value, + rowNumberInAllBlocks() AS row_number + FROM + (SELECT sum(total) AS count, + day_start AS day_start, + breakdown_value AS breakdown_value + FROM + (SELECT count(DISTINCT e__pdi.person_id) AS total, + toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, + ifNull(nullIf(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), ''), '$$_posthog_breakdown_null_$$') AS breakdown_value + FROM events AS e SAMPLE 1.0 + INNER JOIN + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + person_distinct_id2.distinct_id AS distinct_id + FROM person_distinct_id2 + WHERE equals(person_distinct_id2.team_id, 2) + GROUP BY person_distinct_id2.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up')) + GROUP BY day_start, + breakdown_value) + GROUP BY day_start, + breakdown_value + ORDER BY day_start ASC, breakdown_value ASC) + GROUP BY breakdown_value + ORDER BY if(ifNull(equals(breakdown_value, '$$_posthog_breakdown_other_$$'), 0), 2, if(ifNull(equals(breakdown_value, '$$_posthog_breakdown_null_$$'), 0), 1, 0)) ASC, arraySum(total) DESC, breakdown_value ASC) + WHERE isNotNull(breakdown_value) + GROUP BY breakdown_value + ORDER BY if(ifNull(equals(breakdown_value, '$$_posthog_breakdown_other_$$'), 0), 2, if(ifNull(equals(breakdown_value, '$$_posthog_breakdown_null_$$'), 0), 1, 0)) ASC, arraySum(total) DESC, breakdown_value ASC + LIMIT 50000 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1, + format_csv_allow_double_quotes=0, + max_ast_elements=4000000, + max_expanded_ast_elements=4000000, + max_bytes_before_external_group_by=0 + ''' +# --- # name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.3 ''' SELECT groupArray(1)(date)[1] AS date, @@ -1027,6 +1204,113 @@ max_bytes_before_external_group_by=0 ''' # --- +# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.4 + ''' + SELECT groupArray(1)(date)[1] AS date, + arrayFold((acc, x) -> arrayMap(i -> plus(acc[i], x[i]), range(1, plus(length(date), 1))), groupArray(total), arrayWithConstant(length(date), reinterpretAsFloat64(0))) AS total, + arrayMap(i -> if(ifNull(ifNull(greaterOrEquals(row_number, 25), 0), 0), '$$_posthog_breakdown_other_$$', i), breakdown_value) AS breakdown_value + FROM + (SELECT arrayMap(number -> plus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))))), 1))) AS date, + arrayMap(_match_date -> arraySum(arraySlice(groupArray(count), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> ifNull(equals(x, _match_date), isNull(x) + and isNull(_match_date)), _days_for_count), _index), 1))), date) AS total, + breakdown_value AS breakdown_value, + rowNumberInAllBlocks() AS row_number + FROM + (SELECT sum(total) AS count, + day_start AS day_start, + [ifNull(toString(breakdown_value_1), '$$_posthog_breakdown_null_$$')] AS breakdown_value + FROM + (SELECT count(DISTINCT e__pdi.person_id) AS total, + toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, + ifNull(nullIf(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), ''), '$$_posthog_breakdown_null_$$') AS breakdown_value_1 + FROM events AS e SAMPLE 1.0 + INNER JOIN + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + person_distinct_id2.distinct_id AS distinct_id + FROM person_distinct_id2 + WHERE equals(person_distinct_id2.team_id, 2) + GROUP BY person_distinct_id2.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up')) + GROUP BY day_start, + breakdown_value_1) + GROUP BY day_start, + breakdown_value_1 + ORDER BY day_start ASC, breakdown_value ASC) + GROUP BY breakdown_value + ORDER BY if(has(breakdown_value, '$$_posthog_breakdown_other_$$'), 2, if(has(breakdown_value, '$$_posthog_breakdown_null_$$'), 1, 0)) ASC, arraySum(total) DESC, breakdown_value ASC) + WHERE arrayExists(x -> isNotNull(x), breakdown_value) + GROUP BY breakdown_value + ORDER BY if(has(breakdown_value, '$$_posthog_breakdown_other_$$'), 2, if(has(breakdown_value, '$$_posthog_breakdown_null_$$'), 1, 0)) ASC, arraySum(total) DESC, breakdown_value ASC + LIMIT 50000 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1, + format_csv_allow_double_quotes=0, + max_ast_elements=4000000, + max_expanded_ast_elements=4000000, + max_bytes_before_external_group_by=0 + ''' +# --- +# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.5 + ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.6 + ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.7 + ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.8 + ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.9 + ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- # name: TestTrends.test_filter_events_by_precalculated_cohort ''' @@ -1642,6 +1926,18 @@ # --- # name: TestTrends.test_person_filtering_in_cohort_in_action ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_person_filtering_in_cohort_in_action.1 + ''' SELECT count(DISTINCT person_id) FROM cohortpeople @@ -1650,7 +1946,7 @@ AND version = NULL ''' # --- -# name: TestTrends.test_person_filtering_in_cohort_in_action.1 +# name: TestTrends.test_person_filtering_in_cohort_in_action.2 ''' /* cohort_calculation: */ SELECT count(DISTINCT person_id) @@ -1660,7 +1956,7 @@ AND version = 0 ''' # --- -# name: TestTrends.test_person_filtering_in_cohort_in_action.2 +# name: TestTrends.test_person_filtering_in_cohort_in_action.3 ''' SELECT groupArray(1)(date)[1] AS date, arrayFold((acc, x) -> arrayMap(i -> plus(acc[i], x[i]), range(1, plus(length(date), 1))), groupArray(total), arrayWithConstant(length(date), reinterpretAsFloat64(0))) AS total, @@ -1712,6 +2008,18 @@ # --- # name: TestTrends.test_person_filtering_in_cohort_in_action_poe_v2 ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_person_filtering_in_cohort_in_action_poe_v2.1 + ''' SELECT count(DISTINCT person_id) FROM cohortpeople @@ -1720,7 +2028,7 @@ AND version = NULL ''' # --- -# name: TestTrends.test_person_filtering_in_cohort_in_action_poe_v2.1 +# name: TestTrends.test_person_filtering_in_cohort_in_action_poe_v2.2 ''' /* cohort_calculation: */ SELECT count(DISTINCT person_id) @@ -1730,7 +2038,7 @@ AND version = 0 ''' # --- -# name: TestTrends.test_person_filtering_in_cohort_in_action_poe_v2.2 +# name: TestTrends.test_person_filtering_in_cohort_in_action_poe_v2.3 ''' SELECT groupArray(1)(date)[1] AS date, arrayFold((acc, x) -> arrayMap(i -> plus(acc[i], x[i]), range(1, plus(length(date), 1))), groupArray(total), arrayWithConstant(length(date), reinterpretAsFloat64(0))) AS total, @@ -3494,6 +3802,18 @@ ''' # --- # name: TestTrends.test_trends_any_event_total_count + ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_trends_any_event_total_count.1 ''' SELECT arrayMap(number -> plus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))))), 1))) AS date, arrayMap(_match_date -> arraySum(arraySlice(groupArray(count), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> ifNull(equals(x, _match_date), isNull(x) @@ -3519,7 +3839,7 @@ max_bytes_before_external_group_by=0 ''' # --- -# name: TestTrends.test_trends_any_event_total_count.1 +# name: TestTrends.test_trends_any_event_total_count.2 ''' SELECT arrayMap(number -> plus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))))), 1))) AS date, arrayMap(_match_date -> arraySum(arraySlice(groupArray(count), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> ifNull(equals(x, _match_date), isNull(x) @@ -3546,6 +3866,18 @@ ''' # --- # name: TestTrends.test_trends_breakdown_cumulative + ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_trends_breakdown_cumulative.1 ''' SELECT groupArray(1)(date)[1] AS date, arrayFold((acc, x) -> arrayMap(i -> plus(acc[i], x[i]), range(1, plus(length(date), 1))), groupArray(total), arrayWithConstant(length(date), reinterpretAsFloat64(0))) AS total, @@ -3599,6 +3931,18 @@ ''' # --- # name: TestTrends.test_trends_breakdown_cumulative_poe_v2 + ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_trends_breakdown_cumulative_poe_v2.1 ''' SELECT groupArray(1)(date)[1] AS date, arrayFold((acc, x) -> arrayMap(i -> plus(acc[i], x[i]), range(1, plus(length(date), 1))), groupArray(total), arrayWithConstant(length(date), reinterpretAsFloat64(0))) AS total, @@ -3891,6 +4235,18 @@ ''' # --- # name: TestTrends.test_trends_compare_day_interval_relative_range + ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_trends_compare_day_interval_relative_range.1 ''' SELECT arrayMap(number -> plus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))))), 1))) AS date, arrayMap(_match_date -> arraySum(arraySlice(groupArray(count), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> ifNull(equals(x, _match_date), isNull(x) @@ -3916,7 +4272,7 @@ max_bytes_before_external_group_by=0 ''' # --- -# name: TestTrends.test_trends_compare_day_interval_relative_range.1 +# name: TestTrends.test_trends_compare_day_interval_relative_range.2 ''' SELECT arrayMap(number -> plus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-21 00:00:00', 6, 'UTC'))), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-21 00:00:00', 6, 'UTC'))), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 23:59:59', 6, 'UTC'))))), 1))) AS date, arrayMap(_match_date -> arraySum(arraySlice(groupArray(count), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> ifNull(equals(x, _match_date), isNull(x) @@ -3942,7 +4298,7 @@ max_bytes_before_external_group_by=0 ''' # --- -# name: TestTrends.test_trends_compare_day_interval_relative_range.2 +# name: TestTrends.test_trends_compare_day_interval_relative_range.3 ''' SELECT arrayMap(number -> plus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))))), 1))) AS date, arrayMap(_match_date -> arraySum(arraySlice(groupArray(count), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> ifNull(equals(x, _match_date), isNull(x) @@ -4209,6 +4565,18 @@ ''' # --- # name: TestTrends.test_trends_per_day_cumulative + ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_trends_per_day_cumulative.1 ''' SELECT arrayMap(number -> plus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))))), 1))) AS date, arrayFill(x -> ifNull(greater(x, 0), 0), arrayMap(_match_date -> arraySum(arraySlice(groupArray(count), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> ifNull(equals(x, _match_date), isNull(x) @@ -4240,6 +4608,18 @@ ''' # --- # name: TestTrends.test_trends_per_day_dau_cumulative + ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_trends_per_day_dau_cumulative.1 ''' SELECT arrayMap(number -> plus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))))), 1))) AS date, arrayFill(x -> ifNull(greater(x, 0), 0), arrayMap(_match_date -> arraySum(arraySlice(groupArray(count), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> ifNull(equals(x, _match_date), isNull(x) diff --git a/posthog/models/activity_logging/activity_log.py b/posthog/models/activity_logging/activity_log.py index 82c13105f1ebb..3b8489d897645 100644 --- a/posthog/models/activity_logging/activity_log.py +++ b/posthog/models/activity_logging/activity_log.py @@ -3,6 +3,7 @@ from datetime import datetime from decimal import Decimal from typing import Any, Literal, Optional, Union +from uuid import UUID import structlog from django.core.paginator import Paginator @@ -40,6 +41,7 @@ "SessionRecordingPlaylist", "Comment", "Team", + "Project", ] ChangeAction = Literal["changed", "created", "deleted", "merged", "split", "exported"] @@ -219,6 +221,7 @@ class Meta: "property_type_format", ], "Team": ["uuid", "updated_at", "api_token", "created_at", "id"], + "Project": ["id", "created_at"], } @@ -368,10 +371,10 @@ def dict_changes_between( def log_activity( *, - organization_id: Optional[UUIDT], + organization_id: Optional[UUID], team_id: int, user: Optional[User], - item_id: Optional[Union[int, str, UUIDT]], + item_id: Optional[Union[int, str, UUID]], scope: str, activity: str, detail: Detail, diff --git a/posthog/models/organization.py b/posthog/models/organization.py index e64f45ff8abc4..cf1cd5c26986a 100644 --- a/posthog/models/organization.py +++ b/posthog/models/organization.py @@ -72,7 +72,9 @@ def bootstrap( with transaction.atomic(using=self.db): organization = Organization.objects.create(**kwargs) - _, team = Project.objects.create_with_team(organization=organization, team_fields=team_fields) + _, team = Project.objects.create_with_team( + initiating_user=user, organization=organization, team_fields=team_fields + ) organization_membership: Optional[OrganizationMembership] = None if user is not None: organization_membership = OrganizationMembership.objects.create( diff --git a/posthog/models/project.py b/posthog/models/project.py index 5bf82245db590..edd1fdff4edcc 100644 --- a/posthog/models/project.py +++ b/posthog/models/project.py @@ -1,4 +1,5 @@ from typing import TYPE_CHECKING, Optional, cast +from functools import cached_property from django.db import models from django.db import transaction from django.core.validators import MinLengthValidator @@ -6,18 +7,29 @@ from posthog.models.utils import sane_repr if TYPE_CHECKING: - from .team import Team + from posthog.models import Team, User class ProjectManager(models.Manager): - def create_with_team(self, team_fields: Optional[dict] = None, **kwargs) -> tuple["Project", "Team"]: + def create_with_team( + self, *, team_fields: Optional[dict] = None, initiating_user: Optional["User"], **kwargs + ) -> tuple["Project", "Team"]: from .team import Team + if team_fields is None: + team_fields = {} + if "name" in kwargs and "name" not in team_fields: + team_fields["name"] = kwargs["name"] + with transaction.atomic(using=self.db): common_id = Team.objects.increment_id_sequence() project = cast("Project", self.create(id=common_id, **kwargs)) - team = Team.objects.create( - id=common_id, organization=project.organization, project=project, **(team_fields or {}) + team = Team.objects.create_with_data( + id=common_id, + organization_id=project.organization_id, + project=project, + initiating_user=initiating_user, + **team_fields, ) return project, team @@ -25,10 +37,10 @@ def create_with_team(self, team_fields: Optional[dict] = None, **kwargs) -> tupl class Project(models.Model): """DO NOT USE YET - you probably mean the `Team` model instead. - `Project` is part of the environemnts feature, which is a work in progress. + `Project` is part of the environments feature, which is a work in progress. """ - id = models.BigIntegerField(primary_key=True, verbose_name="ID") + id = models.BigIntegerField(primary_key=True, verbose_name="ID") # Same as Team.id field organization = models.ForeignKey( "posthog.Organization", on_delete=models.CASCADE, @@ -50,3 +62,7 @@ def __str__(self): return str(self.pk) __repr__ = sane_repr("id", "name") + + @cached_property + def passthrough_team(self) -> "Team": + return self.teams.get(pk=self.pk) diff --git a/posthog/models/team/team.py b/posthog/models/team/team.py index 3aaedbcd5a6fa..3bce7301ea23d 100644 --- a/posthog/models/team/team.py +++ b/posthog/models/team/team.py @@ -1,9 +1,10 @@ import re from decimal import Decimal from functools import lru_cache -from typing import TYPE_CHECKING, Any, Optional, cast +from typing import TYPE_CHECKING, Optional, cast +from uuid import UUID from zoneinfo import ZoneInfo - +from django.core.cache import cache import posthoganalytics import pydantic import pytz @@ -26,6 +27,7 @@ from posthog.models.filters.mixins.utils import cached_property from posthog.models.filters.utils import GroupTypeIndex from posthog.models.instance_setting import get_instance_setting +from posthog.models.organization import OrganizationMembership from posthog.models.signals import mutable_receiver from posthog.models.utils import ( UUIDClassicModel, @@ -65,7 +67,7 @@ class TeamManager(models.Manager): def get_queryset(self): return super().get_queryset().defer(*DEPRECATED_ATTRS) - def set_test_account_filters(self, organization: Optional[Any]) -> list: + def set_test_account_filters(self, organization_id: Optional[UUID]) -> list: filters = [ { "key": "$host", @@ -74,10 +76,12 @@ def set_test_account_filters(self, organization: Optional[Any]) -> list: "type": "event", } ] - if organization: - example_emails = organization.members.only("email") + if organization_id: + example_emails_raw = OrganizationMembership.objects.filter(organization_id=organization_id).values_list( + "user__email", flat=True + ) generic_emails = GenericEmails() - example_emails = [email.email for email in example_emails if not generic_emails.is_generic(email.email)] + example_emails = [email for email in example_emails_raw if not generic_emails.is_generic(email)] if len(example_emails) > 0: example_email = re.search(r"@[\w.]+", example_emails[0]) if example_email: @@ -87,16 +91,25 @@ def set_test_account_filters(self, organization: Optional[Any]) -> list: ] return filters - def create_with_data(self, user: Any = None, default_dashboards: bool = True, **kwargs) -> "Team": - kwargs["test_account_filters"] = self.set_test_account_filters(kwargs.get("organization")) + def create_with_data(self, *, initiating_user: Optional["User"], **kwargs) -> "Team": team = cast("Team", self.create(**kwargs)) - # Create default dashboards (skipped for demo projects) - if default_dashboards: - dashboard = Dashboard.objects.db_manager(self.db).create(name="My App Dashboard", pinned=True, team=team) - create_dashboard_from_template("DEFAULT_APP", dashboard) - team.primary_dashboard = dashboard - team.save() + if kwargs.get("is_demo"): + if initiating_user is None: + raise ValueError("initiating_user must be provided when creating a demo team") + team.kick_off_demo_data_generation(initiating_user) + return team # Return quickly, as the demo data and setup will be created asynchronously + + team.test_account_filters = self.set_test_account_filters( + kwargs.get("organization_id") or kwargs["organization"].id + ) + + # Create default dashboards + dashboard = Dashboard.objects.db_manager(self.db).create(name="My App Dashboard", pinned=True, team=team) + create_dashboard_from_template("DEFAULT_APP", dashboard) + team.primary_dashboard = dashboard + + team.save() return team def create(self, **kwargs): @@ -447,6 +460,44 @@ def path_cleaning_filter_models(self) -> list[PathCleaningFilter]: continue return filters + def reset_token_and_save(self, *, user: "User", is_impersonated_session: bool): + from posthog.models.activity_logging.activity_log import Change, Detail, log_activity + + old_token = self.api_token + self.api_token = generate_random_token_project() + self.save() + set_team_in_cache(old_token, None) + log_activity( + organization_id=self.organization_id, + team_id=self.pk, + user=cast("User", user), + was_impersonated=is_impersonated_session, + scope="Team", + item_id=self.pk, + activity="updated", + detail=Detail( + name=str(self.name), + changes=[ + Change( + type="Team", + action="changed", + field="api_token", + ) + ], + ), + ) + + def get_is_generating_demo_data(self) -> bool: + cache_key = f"is_generating_demo_data_{self.id}" + return cache.get(cache_key) == "True" + + def kick_off_demo_data_generation(self, initiating_user: "User") -> None: + from posthog.tasks.demo_create_data import create_data_for_demo_team + + cache_key = f"is_generating_demo_data_{self.id}" + cache.set(cache_key, "True") # Create an item in the cache that we can use to see if the demo data is ready + create_data_for_demo_team.delay(self.id, initiating_user.id, cache_key) + def all_users_with_access(self) -> QuerySet["User"]: from ee.models.explicit_team_membership import ExplicitTeamMembership from posthog.models.organization import OrganizationMembership diff --git a/posthog/models/test/test_project.py b/posthog/models/test/test_project.py index d6bfe0ed3a36a..1fd7434f90da3 100644 --- a/posthog/models/test/test_project.py +++ b/posthog/models/test/test_project.py @@ -7,6 +7,7 @@ class TestProject(BaseTest): def test_create_project_with_team_no_team_fields(self): project, team = Project.objects.create_with_team( + initiating_user=self.user, organization=self.organization, name="Test project", ) @@ -17,13 +18,14 @@ def test_create_project_with_team_no_team_fields(self): self.assertEqual( team.name, - "Default project", # TODO: When Environments are rolled out, ensure this says "Default environment" + "Test project", # TODO: When Environments are rolled out, ensure this says "Default environment" ) self.assertEqual(team.organization, self.organization) self.assertEqual(team.project, project) def test_create_project_with_team_with_team_fields(self): project, team = Project.objects.create_with_team( + initiating_user=self.user, organization=self.organization, name="Test project", team_fields={"name": "Test team", "access_control": True}, @@ -42,6 +44,7 @@ def test_create_project_with_team_uses_team_id_sequence(self): expected_common_id = Team.objects.increment_id_sequence() + 1 project, team = Project.objects.create_with_team( + initiating_user=self.user, organization=self.organization, name="Test project", team_fields={"name": "Test team", "access_control": True}, @@ -64,6 +67,7 @@ def test_create_project_with_team_does_not_create_if_team_fails(self, mock_creat with self.assertRaises(Exception): Project.objects.create_with_team( + initiating_user=self.user, organization=self.organization, name="Test project", team_fields={"name": "Test team", "access_control": True}, diff --git a/posthog/models/user.py b/posthog/models/user.py index 621c1d36429a7..748533be437cd 100644 --- a/posthog/models/user.py +++ b/posthog/models/user.py @@ -80,7 +80,9 @@ def bootstrap( if create_team: team = create_team(organization, user) else: - team = Team.objects.create_with_data(user=user, organization=organization, **(team_fields or {})) + team = Team.objects.create_with_data( + initiating_user=user, organization=organization, **(team_fields or {}) + ) user.join(organization=organization, level=OrganizationMembership.Level.OWNER) return organization, team, user diff --git a/posthog/permissions.py b/posthog/permissions.py index d7c6bd4cf81d9..6a2a3c14cb490 100644 --- a/posthog/permissions.py +++ b/posthog/permissions.py @@ -173,11 +173,7 @@ class TeamMemberLightManagementPermission(BasePermission): def has_permission(self, request, view) -> bool: try: - if request.resolver_match.url_name.startswith("team-"): - # /projects/ endpoint handling - team = view.get_object() - else: - team = view.team + team = view.team except Team.DoesNotExist: return True # This will be handled as a 404 in the viewset requesting_level = view.user_permissions.team(team).effective_membership_level diff --git a/posthog/queries/test/__snapshots__/test_trends.ambr b/posthog/queries/test/__snapshots__/test_trends.ambr index 6e60ae26b943e..81808cef8269b 100644 --- a/posthog/queries/test/__snapshots__/test_trends.ambr +++ b/posthog/queries/test/__snapshots__/test_trends.ambr @@ -853,6 +853,18 @@ # --- # name: TestTrends.test_dau_with_breakdown_filtering_with_sampling ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.1 + ''' SELECT replaceRegexpAll(JSONExtractRaw(properties, '$some_property'), '^"|"$', '') AS value, count(*) as count @@ -867,7 +879,7 @@ OFFSET 0 ''' # --- -# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.1 +# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.2 ''' SELECT groupArray(day_start) as date, @@ -919,7 +931,7 @@ ORDER BY breakdown_value ''' # --- -# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.2 +# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.3 ''' SELECT replaceRegexpAll(JSONExtractRaw(properties, '$some_property'), '^"|"$', '') AS value, @@ -935,7 +947,7 @@ OFFSET 0 ''' # --- -# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.3 +# name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.4 ''' SELECT groupArray(day_start) as date, @@ -1480,6 +1492,18 @@ # --- # name: TestTrends.test_person_filtering_in_cohort_in_action ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_person_filtering_in_cohort_in_action.1 + ''' SELECT replaceRegexpAll(JSONExtractRaw(properties, '$some_property'), '^"|"$', '') AS value, count(*) as count @@ -1513,7 +1537,7 @@ OFFSET 0 ''' # --- -# name: TestTrends.test_person_filtering_in_cohort_in_action.1 +# name: TestTrends.test_person_filtering_in_cohort_in_action.2 ''' SELECT groupArray(day_start) as date, @@ -1579,6 +1603,18 @@ # --- # name: TestTrends.test_person_filtering_in_cohort_in_action_poe_v2 ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_person_filtering_in_cohort_in_action_poe_v2.1 + ''' SELECT replaceRegexpAll(JSONExtractRaw(properties, '$some_property'), '^"|"$', '') AS value, count(*) as count @@ -1613,7 +1649,7 @@ OFFSET 0 ''' # --- -# name: TestTrends.test_person_filtering_in_cohort_in_action_poe_v2.1 +# name: TestTrends.test_person_filtering_in_cohort_in_action_poe_v2.2 ''' SELECT groupArray(day_start) as date, @@ -3970,6 +4006,18 @@ # --- # name: TestTrends.test_trends_any_event_total_count ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_trends_any_event_total_count.1 + ''' SELECT groupArray(day_start) as date, groupArray(count) AS total @@ -3994,7 +4042,7 @@ ORDER BY day_start) ''' # --- -# name: TestTrends.test_trends_any_event_total_count.1 +# name: TestTrends.test_trends_any_event_total_count.2 ''' SELECT groupArray(day_start) as date, @@ -4022,6 +4070,18 @@ # --- # name: TestTrends.test_trends_breakdown_cumulative ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_trends_breakdown_cumulative.1 + ''' SELECT replaceRegexpAll(JSONExtractRaw(properties, '$some_property'), '^"|"$', '') AS value, count(*) as count @@ -4036,7 +4096,7 @@ OFFSET 0 ''' # --- -# name: TestTrends.test_trends_breakdown_cumulative.1 +# name: TestTrends.test_trends_breakdown_cumulative.2 ''' SELECT groupArray(day_start) as date, @@ -4098,6 +4158,18 @@ # --- # name: TestTrends.test_trends_breakdown_cumulative_poe_v2 ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_trends_breakdown_cumulative_poe_v2.1 + ''' SELECT replaceRegexpAll(JSONExtractRaw(properties, '$some_property'), '^"|"$', '') AS value, count(*) as count @@ -4120,7 +4192,7 @@ OFFSET 0 ''' # --- -# name: TestTrends.test_trends_breakdown_cumulative_poe_v2.1 +# name: TestTrends.test_trends_breakdown_cumulative_poe_v2.2 ''' SELECT groupArray(day_start) as date, @@ -4298,6 +4370,18 @@ # --- # name: TestTrends.test_trends_compare_day_interval_relative_range ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_trends_compare_day_interval_relative_range.1 + ''' SELECT groupArray(day_start) as date, groupArray(count) AS total @@ -4322,7 +4406,7 @@ ORDER BY day_start) ''' # --- -# name: TestTrends.test_trends_compare_day_interval_relative_range.1 +# name: TestTrends.test_trends_compare_day_interval_relative_range.2 ''' SELECT groupArray(day_start) as date, @@ -4348,7 +4432,7 @@ ORDER BY day_start) ''' # --- -# name: TestTrends.test_trends_compare_day_interval_relative_range.2 +# name: TestTrends.test_trends_compare_day_interval_relative_range.3 ''' SELECT groupArray(day_start) as date, @@ -4657,6 +4741,18 @@ # --- # name: TestTrends.test_trends_per_day_cumulative ''' + /* celery:posthog.tasks.tasks.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ''' +# --- +# name: TestTrends.test_trends_per_day_cumulative.1 + ''' SELECT groupArray(day_start) as date, groupArray(count) AS total diff --git a/posthog/test/base.py b/posthog/test/base.py index 451264bfc205b..0b7ccd78a85dc 100644 --- a/posthog/test/base.py +++ b/posthog/test/base.py @@ -107,20 +107,21 @@ def _setup_test_data(klass): klass.organization = Organization.objects.create(name=klass.CONFIG_ORGANIZATION_NAME) - klass.project, klass.team = Project.objects.create_with_team( + klass.project = Project.objects.create(id=Team.objects.increment_id_sequence(), organization=klass.organization) + klass.team = Team.objects.create( + id=klass.project.id, + project=klass.project, organization=klass.organization, - team_fields={ - "api_token": klass.CONFIG_API_TOKEN, - "test_account_filters": [ - { - "key": "email", - "value": "@posthog.com", - "operator": "not_icontains", - "type": "person", - } - ], - "has_completed_onboarding_for": {"product_analytics": True}, - }, + api_token=klass.CONFIG_API_TOKEN, + test_account_filters=[ + { + "key": "email", + "value": "@posthog.com", + "operator": "not_icontains", + "type": "person", + } + ], + has_completed_onboarding_for={"product_analytics": True}, ) if klass.CONFIG_EMAIL: klass.user = User.objects.create_and_join(klass.organization, klass.CONFIG_EMAIL, klass.CONFIG_PASSWORD) diff --git a/posthog/test/test_middleware.py b/posthog/test/test_middleware.py index ce8bfeb71b7bb..f5b4190ef4293 100644 --- a/posthog/test/test_middleware.py +++ b/posthog/test/test_middleware.py @@ -124,7 +124,7 @@ class TestAutoProjectMiddleware(APIBaseTest): @classmethod def setUpTestData(cls): super().setUpTestData() - cls.base_app_num_queries = 40 + cls.base_app_num_queries = 45 # Create another team that the user does have access to cls.second_team = create_team(organization=cls.organization, name="Second Life") diff --git a/posthog/test/test_team.py b/posthog/test/test_team.py index 076fc21e5fe34..6894fb6134642 100644 --- a/posthog/test/test_team.py +++ b/posthog/test/test_team.py @@ -76,7 +76,7 @@ def test_team_has_expected_defaults(self): self.assertEqual(team.autocapture_exceptions_errors_to_ignore, None) def test_create_team_with_test_account_filters(self): - team = Team.objects.create_with_data(organization=self.organization) + team = Team.objects.create_with_data(initiating_user=self.user, organization=self.organization) self.assertEqual( team.test_account_filters, [ @@ -99,7 +99,7 @@ def test_create_team_with_test_account_filters(self): user = User.objects.create(email="test@gmail.com") organization = Organization.objects.create() organization.members.set([user]) - team = Team.objects.create_with_data(organization=organization) + team = Team.objects.create_with_data(initiating_user=self.user, organization=organization) self.assertEqual( team.test_account_filters, [ @@ -113,7 +113,7 @@ def test_create_team_with_test_account_filters(self): ) def test_create_team_sets_primary_dashboard(self): - team = Team.objects.create_with_data(organization=self.organization) + team = Team.objects.create_with_data(initiating_user=self.user, organization=self.organization) self.assertIsInstance(team.primary_dashboard, Dashboard) # Ensure insights are created and linked @@ -139,7 +139,7 @@ def test_preinstalled_are_autoenabled(self, mock_get): def test_team_on_cloud_uses_feature_flag_to_determine_person_on_events(self, mock_feature_enabled): with self.is_cloud(True): with override_instance_config("PERSON_ON_EVENTS_ENABLED", False): - team = Team.objects.create_with_data(organization=self.organization) + team = Team.objects.create_with_data(initiating_user=self.user, organization=self.organization) self.assertEqual( team.person_on_events_mode, PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS ) @@ -162,7 +162,7 @@ def test_team_on_cloud_uses_feature_flag_to_determine_person_on_events(self, moc def test_team_on_self_hosted_uses_instance_setting_to_determine_person_on_events(self, mock_feature_enabled): with self.is_cloud(False): with override_instance_config("PERSON_ON_EVENTS_V2_ENABLED", True): - team = Team.objects.create_with_data(organization=self.organization) + team = Team.objects.create_with_data(initiating_user=self.user, organization=self.organization) self.assertEqual( team.person_on_events_mode, PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS ) @@ -171,7 +171,7 @@ def test_team_on_self_hosted_uses_instance_setting_to_determine_person_on_events assert args_list[0][0] != "persons-on-events-v2-reads-enabled" with override_instance_config("PERSON_ON_EVENTS_V2_ENABLED", False): - team = Team.objects.create_with_data(organization=self.organization) + team = Team.objects.create_with_data(initiating_user=self.user, organization=self.organization) self.assertEqual(team.person_on_events_mode, PersonsOnEventsMode.DISABLED) for args_list in mock_feature_enabled.call_args_list: # It is ok if we check other feature flags, just not `persons-on-events-v2-reads-enabled` @@ -179,7 +179,7 @@ def test_team_on_self_hosted_uses_instance_setting_to_determine_person_on_events def test_each_team_gets_project_with_default_name_and_same_id(self): # Can be removed once environments are fully rolled out - team = Team.objects.create_with_data(organization=self.organization) + team = Team.objects.create_with_data(initiating_user=self.user, organization=self.organization) project = Project.objects.filter(id=team.id).first() @@ -188,7 +188,7 @@ def test_each_team_gets_project_with_default_name_and_same_id(self): def test_each_team_gets_project_with_custom_name_and_same_id(self): # Can be removed once environments are fully rolled out - team = Team.objects.create_with_data(organization=self.organization, name="Hogflix") + team = Team.objects.create_with_data(organization=self.organization, initiating_user=self.user, name="Hogflix") project = Project.objects.filter(id=team.id).first() @@ -203,7 +203,7 @@ def test_team_not_created_if_project_creation_fails(self, mock_create): initial_project_count = Project.objects.count() with self.assertRaises(Exception): - Team.objects.create_with_data(organization=self.organization, name="Hogflix") + Team.objects.create_with_data(organization=self.organization, initiating_user=self.user, name="Hogflix") self.assertEqual(Team.objects.count(), initial_team_count) self.assertEqual(Project.objects.count(), initial_project_count) diff --git a/posthog/urls.py b/posthog/urls.py index c34956e8131b3..69eaddf1c9717 100644 --- a/posthog/urls.py +++ b/posthog/urls.py @@ -28,10 +28,6 @@ authentication, capture, decide, - organizations_router, - project_dashboards_router, - project_feature_flags_router, - projects_router, router, sharing, signup, @@ -71,13 +67,7 @@ logger.warn(f"Could not import ee.urls", exc_info=True) pass else: - extend_api_router( - router, - projects_router=projects_router, - organizations_router=organizations_router, - project_dashboards_router=project_dashboards_router, - project_feature_flags_router=project_feature_flags_router, - ) + extend_api_router() @requires_csrf_token diff --git a/posthog/utils.py b/posthog/utils.py index aaf02658b42d1..0e2f5a6b30f38 100644 --- a/posthog/utils.py +++ b/posthog/utils.py @@ -349,12 +349,14 @@ def render_template( if not request.GET.get("no-preloaded-app-context"): from posthog.api.shared import TeamPublicSerializer from posthog.api.team import TeamSerializer + from posthog.api.project import ProjectSerializer from posthog.api.user import UserSerializer from posthog.user_permissions import UserPermissions from posthog.views import preflight_check posthog_app_context = { "current_user": None, + "current_project": None, "current_team": None, "preflight": json.loads(preflight_check(request).getvalue()), "default_event_name": "$pageview", @@ -386,6 +388,12 @@ def render_template( many=False, ) posthog_app_context["current_team"] = team_serialized.data + project_serialized = ProjectSerializer( + user.team.project, + context={"request": request, "user_permissions": user_permissions}, + many=False, + ) + posthog_app_context["current_project"] = project_serialized.data posthog_app_context["frontend_apps"] = get_frontend_apps(user.team.pk) posthog_app_context["default_event_name"] = get_default_event_name(user.team) diff --git a/posthog/warehouse/api/test/test_external_data_source.py b/posthog/warehouse/api/test/test_external_data_source.py index 8e295ac2e925c..8a455a7b89883 100644 --- a/posthog/warehouse/api/test/test_external_data_source.py +++ b/posthog/warehouse/api/test/test_external_data_source.py @@ -621,7 +621,7 @@ def test_internal_postgres(self, patch_get_sql_schemas_for_source_type): } ] - new_team = Team.objects.create(name="new_team", organization=self.team.organization) + new_team = Team.objects.create(id=984961485, name="new_team", organization=self.team.organization) response = self.client.post( f"/api/projects/{new_team.pk}/external_data_sources/database_schema/", @@ -665,7 +665,7 @@ def test_internal_postgres(self, patch_get_sql_schemas_for_source_type): } ] - new_team = Team.objects.create(name="new_team", organization=self.team.organization) + new_team = Team.objects.create(id=984961486, name="new_team", organization=self.team.organization) response = self.client.post( f"/api/projects/{new_team.pk}/external_data_sources/database_schema/",