From d495b158c1d9d25df6ca98a4eb74b544cd825d23 Mon Sep 17 00:00:00 2001 From: Manuel Raynaud Date: Thu, 28 May 2026 14:56:08 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=88(backend)=20capture=20an=20event=20?= =?UTF-8?q?when=20a=20document=20is=20created=20and=20deleted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a document is created and deleted, we capture an event and we send it to posthog. --- src/backend/core/api/serializers.py | 3 ++ src/backend/core/api/viewsets.py | 16 +++++++ .../test_api_documents_children_create.py | 22 +++++++--- .../documents/test_api_documents_create.py | 24 +++++++---- .../test_api_documents_create_for_owner.py | 42 +++++++++++++------ .../documents/test_api_documents_delete.py | 17 ++++++-- 6 files changed, 96 insertions(+), 28 deletions(-) diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index 2152ce817..9253a1cfb 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -23,6 +23,7 @@ from core.services.converter_services import ( ConversionError, Converter, ) +from core.utils.analytics import PosthogEventName, posthog_capture from core.utils.treebeard import create_tree_node_with_retry @@ -490,6 +491,8 @@ class ServerCreateDocumentSerializer(serializers.Serializer): ) ) + posthog_capture(PosthogEventName.DOC_CREATED, user, {}, document=document) + if user: # Associate the document with the pre-existing user models.DocumentAccess.objects.create( diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index e71e468e0..e355f2d3a 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -67,6 +67,7 @@ from core.services.search_indexers import ( get_visited_document_ids_of, ) from core.tasks.mail import send_ask_for_access_mail +from core.utils.analytics import PosthogEventName, posthog_capture from core.utils.paths import filter_descendants from core.utils.treebeard import create_tree_node_with_retry from core.utils.users import users_sharing_documents_with @@ -778,10 +779,18 @@ class DocumentViewSet( role=models.RoleChoices.OWNER, ) + posthog_capture( + PosthogEventName.DOC_CREATED, self.request.user, {}, document=obj + ) + def perform_destroy(self, instance): """Override to implement a soft delete instead of dumping the record in database.""" instance.soft_delete() + posthog_capture( + PosthogEventName.DOC_DELETED, self.request.user, {}, document=instance + ) + def _can_user_edit_document(self, document_id, set_cache=False): """Check if the user can edit the document.""" try: @@ -1112,6 +1121,13 @@ class DocumentViewSet( # Set the created instance to the serializer serializer.instance = child_document + posthog_capture( + PosthogEventName.DOC_CREATED, + self.request.user, + {"document_parent": str(document.id)}, + document=child_document, + ) + headers = self.get_success_headers(serializer.data) return drf.response.Response( serializer.data, status=status.HTTP_201_CREATED, headers=headers diff --git a/src/backend/core/tests/documents/test_api_documents_children_create.py b/src/backend/core/tests/documents/test_api_documents_children_create.py index 3119055d0..9d98b5472 100644 --- a/src/backend/core/tests/documents/test_api_documents_children_create.py +++ b/src/backend/core/tests/documents/test_api_documents_children_create.py @@ -4,6 +4,7 @@ Tests for Documents API endpoint in impress's core app: children create from concurrent.futures import ThreadPoolExecutor from io import BytesIO +from unittest import mock from unittest.mock import patch from uuid import uuid4 @@ -13,6 +14,7 @@ from rest_framework.test import APIClient from core import factories from core.models import Document, LinkReachChoices, LinkRoleChoices from core.services import mime_types +from core.utils.analytics import PosthogEventName pytestmark = pytest.mark.django_db @@ -105,12 +107,13 @@ def test_api_documents_children_create_authenticated_success(reach, role, depth) parent=document, link_reach="restricted" ) - response = client.post( - f"/api/v1.0/documents/{document.id!s}/children/", - { - "title": "my child", - }, - ) + with mock.patch("core.api.viewsets.posthog_capture") as mock_capture: + response = client.post( + f"/api/v1.0/documents/{document.id!s}/children/", + { + "title": "my child", + }, + ) assert response.status_code == 201 @@ -120,6 +123,13 @@ def test_api_documents_children_create_authenticated_success(reach, role, depth) # Access objects on the child are not necessary assert child.accesses.exists() is False + mock_capture.assert_called_once_with( + PosthogEventName.DOC_CREATED, + user, + {"document_parent": str(document.id)}, + document=child, + ) + @pytest.mark.parametrize("depth", [1, 2, 3]) def test_api_documents_children_create_related_forbidden(depth): diff --git a/src/backend/core/tests/documents/test_api_documents_create.py b/src/backend/core/tests/documents/test_api_documents_create.py index 881ce6cad..c2896e2aa 100644 --- a/src/backend/core/tests/documents/test_api_documents_create.py +++ b/src/backend/core/tests/documents/test_api_documents_create.py @@ -3,6 +3,7 @@ Tests for Documents API endpoint in impress's core app: create """ from concurrent.futures import ThreadPoolExecutor +from unittest import mock from uuid import uuid4 import pytest @@ -10,6 +11,7 @@ from rest_framework.test import APIClient from core import factories from core.models import Document +from core.utils.analytics import PosthogEventName pytestmark = pytest.mark.django_db @@ -37,13 +39,14 @@ def test_api_documents_create_authenticated_success(): client = APIClient() client.force_login(user) - response = client.post( - "/api/v1.0/documents/", - { - "title": "my document", - }, - format="json", - ) + with mock.patch("core.api.viewsets.posthog_capture") as mock_capture: + response = client.post( + "/api/v1.0/documents/", + { + "title": "my document", + }, + format="json", + ) assert response.status_code == 201 document = Document.objects.get() @@ -51,6 +54,13 @@ def test_api_documents_create_authenticated_success(): assert document.link_reach == "restricted" assert document.accesses.filter(role="owner", user=user).exists() + mock_capture.assert_called_once_with( + PosthogEventName.DOC_CREATED, + user, + {}, + document=document, + ) + @pytest.mark.django_db(transaction=True) def test_api_documents_create_document_race_condition(): diff --git a/src/backend/core/tests/documents/test_api_documents_create_for_owner.py b/src/backend/core/tests/documents/test_api_documents_create_for_owner.py index 53aaf1142..09d021ce7 100644 --- a/src/backend/core/tests/documents/test_api_documents_create_for_owner.py +++ b/src/backend/core/tests/documents/test_api_documents_create_for_owner.py @@ -5,6 +5,7 @@ Tests for Documents API endpoint in impress's core app: create # pylint: disable=W0621 from concurrent.futures import ThreadPoolExecutor +from unittest import mock from unittest.mock import patch from django.core import mail @@ -18,6 +19,7 @@ from core.api.serializers import ServerCreateDocumentSerializer from core.models import Document, Invitation, User from core.services import mime_types from core.services.converter_services import ConversionError, YdocConverter +from core.utils.analytics import PosthogEventName pytestmark = pytest.mark.django_db @@ -183,12 +185,13 @@ def test_api_documents_create_for_owner_existing(mock_convert_md): "email": "irrelevant@example.com", # Should be ignored since the user already exists } - response = APIClient().post( - "/api/v1.0/documents/create-for-owner/", - data, - format="json", - HTTP_AUTHORIZATION="Bearer DummyToken", - ) + with mock.patch("core.api.serializers.posthog_capture") as mock_capture: + response = APIClient().post( + "/api/v1.0/documents/create-for-owner/", + data, + format="json", + HTTP_AUTHORIZATION="Bearer DummyToken", + ) assert response.status_code == 201 @@ -204,6 +207,13 @@ def test_api_documents_create_for_owner_existing(mock_convert_md): assert document.creator == user assert document.accesses.filter(user=user, role="owner").exists() + mock_capture.assert_called_once_with( + PosthogEventName.DOC_CREATED, + user, + {}, + document=document, + ) + assert Invitation.objects.exists() is False assert len(mail.outbox) == 1 @@ -230,12 +240,13 @@ def test_api_documents_create_for_owner_new_user(mock_convert_md): "email": "john.doe@example.com", # Should be used to create a new user } - response = APIClient().post( - "/api/v1.0/documents/create-for-owner/", - data, - format="json", - HTTP_AUTHORIZATION="Bearer DummyToken", - ) + with mock.patch("core.api.serializers.posthog_capture") as mock_capture: + response = APIClient().post( + "/api/v1.0/documents/create-for-owner/", + data, + format="json", + HTTP_AUTHORIZATION="Bearer DummyToken", + ) assert response.status_code == 201 @@ -251,6 +262,13 @@ def test_api_documents_create_for_owner_new_user(mock_convert_md): assert document.creator is None assert document.accesses.exists() is False + mock_capture.assert_called_once_with( + PosthogEventName.DOC_CREATED, + None, + {}, + document=document, + ) + invitation = Invitation.objects.get() assert invitation.email == "john.doe@example.com" assert invitation.role == "owner" diff --git a/src/backend/core/tests/documents/test_api_documents_delete.py b/src/backend/core/tests/documents/test_api_documents_delete.py index 776bbe1fd..f89503eb7 100644 --- a/src/backend/core/tests/documents/test_api_documents_delete.py +++ b/src/backend/core/tests/documents/test_api_documents_delete.py @@ -2,11 +2,14 @@ Tests for Documents API endpoint in impress's core app: delete """ +from unittest import mock + import pytest from rest_framework.test import APIClient from core import factories, models from core.tests.conftest import TEAM, USER, VIA +from core.utils.analytics import PosthogEventName pytestmark = pytest.mark.django_db @@ -127,9 +130,10 @@ def test_api_documents_delete_authenticated_owner(via, mock_user_teams): document=document, team="lasuite", role="owner" ) - response = client.delete( - f"/api/v1.0/documents/{document.id}/", - ) + with mock.patch("core.api.viewsets.posthog_capture") as mock_capture: + response = client.delete( + f"/api/v1.0/documents/{document.id}/", + ) assert response.status_code == 204 @@ -137,3 +141,10 @@ def test_api_documents_delete_authenticated_owner(via, mock_user_teams): assert models.Document.objects.count() == 1 assert models.Document.objects.filter(deleted_at__isnull=True).exists() is False assert models.Document.objects.filter(deleted_at__isnull=False).count() == 1 + + mock_capture.assert_called_once_with( + PosthogEventName.DOC_DELETED, + user, + {}, + document=document, + )