diff --git a/posthog/api/organization_domain.py b/posthog/api/organization_domain.py index e39b611c1fb32..d563c5dd3edf3 100644 --- a/posthog/api/organization_domain.py +++ b/posthog/api/organization_domain.py @@ -1,5 +1,6 @@ import re from typing import Any, cast +import posthoganalytics from rest_framework import exceptions, request, response, serializers from posthog.api.utils import action @@ -11,10 +12,29 @@ from posthog.models import OrganizationDomain from posthog.models.organization import Organization from posthog.permissions import OrganizationAdminWritePermissions +from posthog.event_usage import groups DOMAIN_REGEX = r"^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$" +def _capture_domain_event(request, domain: OrganizationDomain, event_type: str, properties: dict | None = None) -> None: + if not properties: + properties = {} + + properties.update( + { + "domain": domain.domain, + } + ) + + posthoganalytics.capture( + request.user.distinct_id, + f"organization domain {event_type}", + properties=properties, + groups=groups(domain.organization), + ) + + class OrganizationDomainSerializer(serializers.ModelSerializer): UPDATE_ONLY_WHEN_VERIFIED = ["jit_provisioning_enabled", "sso_enforcement"] @@ -96,3 +116,38 @@ def verify(self, request: request.Request, **kw) -> response.Response: serializer = self.get_serializer(instance=instance) return response.Response(serializer.data) + + def create(self, request: request.Request, *args: Any, **kwargs: Any) -> response.Response: + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + + _capture_domain_event( + request, + instance, + "created", + properties={ + "jit_provisioning_enabled": instance.jit_provisioning_enabled, + "sso_enforcement": instance.sso_enforcement or None, + }, + ) + + return response.Response(serializer.data, status=201) + + def destroy(self, request: request.Request, *args: Any, **kwargs: Any) -> response.Response: + instance = self.get_object() + + _capture_domain_event( + request, + instance, + "deleted", + properties={ + "is_verified": instance.is_verified, + "had_saml": instance.has_saml, + "had_jit_provisioning": instance.jit_provisioning_enabled, + "had_sso_enforcement": bool(instance.sso_enforcement), + }, + ) + + instance.delete() + return response.Response(status=204) diff --git a/posthog/api/test/test_organization_domain.py b/posthog/api/test/test_organization_domain.py index 82ded3bc1c7fe..33de9f45facfc 100644 --- a/posthog/api/test/test_organization_domain.py +++ b/posthog/api/test/test_organization_domain.py @@ -1,5 +1,5 @@ import datetime -from unittest.mock import patch +from unittest.mock import ANY, patch from zoneinfo import ZoneInfo import dns.resolver @@ -84,7 +84,8 @@ def test_cannot_list_or_retrieve_domains_for_other_org(self): # Create domains - def test_create_domain(self): + @patch("posthoganalytics.capture") + def test_create_domain(self, mock_capture): self.organization_membership.level = OrganizationMembership.Level.ADMIN self.organization.available_product_features = [ {"key": "automatic_provisioning", "name": "automatic_provisioning"} @@ -116,6 +117,18 @@ def test_create_domain(self): self.assertEqual(instance.last_verification_retry, None) self.assertEqual(instance.sso_enforcement, "") + # Verify the domain creation capture event was called + mock_capture.assert_any_call( + self.user.distinct_id, + "organization domain created", + properties={ + "domain": "the.posthog.com", + "jit_provisioning_enabled": False, + "sso_enforcement": None, + }, + groups={"instance": ANY, "organization": str(self.organization.id)}, + ) + def test_cant_create_domain_without_feature(self): self.organization_membership.level = OrganizationMembership.Level.ADMIN self.organization_membership.save() @@ -439,7 +452,8 @@ def test_cannot_update_domain_for_another_org(self): # Delete domains - def test_admin_can_delete_domain(self): + @patch("posthoganalytics.capture") + def test_admin_can_delete_domain(self, mock_capture): self.organization_membership.level = OrganizationMembership.Level.ADMIN self.organization_membership.save() @@ -449,6 +463,20 @@ def test_admin_can_delete_domain(self): self.assertFalse(OrganizationDomain.objects.filter(id=self.domain.id).exists()) + # Verify the domain deletion capture event was called + mock_capture.assert_any_call( + self.user.distinct_id, + "organization domain deleted", + properties={ + "domain": "myposthog.com", + "is_verified": False, + "had_saml": False, + "had_jit_provisioning": False, + "had_sso_enforcement": False, + }, + groups={"instance": ANY, "organization": str(self.organization.id)}, + ) + def test_only_admin_can_delete_domain(self): response = self.client.delete(f"/api/organizations/@current/domains/{self.domain.id}") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)