📈(backend) capture an event when a document is imported

We want to capture an event when a file is imported. We add in the
properties the type of file imported and if it is in the
create-for-owner api call.
This commit is contained in:
Manuel Raynaud
2026-06-01 17:41:56 +02:00
parent d1476024a7
commit a05de21d14
7 changed files with 291 additions and 116 deletions
+9
View File
@@ -492,6 +492,15 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
)
posthog_capture(PosthogEventName.DOC_CREATED, user, {}, document=document)
posthog_capture(
PosthogEventName.DOC_IMPORTED,
user,
{
"content_type": mime_types.MARKDOWN,
"create_for_owner": True,
},
document=document,
)
if user:
# Associate the document with the pre-existing user
+6
View File
@@ -755,6 +755,12 @@ class DocumentViewSet(
serializer.validated_data["content"] = converted_content
serializer.validated_data["title"] = uploaded_file.name
logger.info("conversion ended successfully")
posthog_capture(
PosthogEventName.DOC_IMPORTED,
self.request.user,
{"content_type": uploaded_file.content_type},
)
except ConversionError as err:
logger.error("could not convert file content with error: %s", err)
raise drf.exceptions.ValidationError(
@@ -330,13 +330,14 @@ def test_api_documents_children_create_with_docx_file_success(mock_convert, sett
parent = factories.DocumentFactory(creator=user, users=[(user, "owner")])
response = client.post(
f"/api/v1.0/documents/{parent.id}/children/",
{
"file": file,
},
format="multipart",
)
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
f"/api/v1.0/documents/{parent.id}/children/",
{
"file": file,
},
format="multipart",
)
assert response.status_code == 201
assert Document.objects.count() == 2
@@ -351,6 +352,13 @@ def test_api_documents_children_create_with_docx_file_success(mock_convert, sett
accept=mime_types.YJS,
)
# The successful conversion should be tracked in PostHog
mock_capture.assert_any_call(
PosthogEventName.DOC_IMPORTED,
user,
{"content_type": mime_types.DOCX},
)
@patch("core.services.converter_services.Converter.convert")
def test_api_documents_children_create_with_docx_file_disabled(mock_convert, settings):
@@ -370,13 +378,14 @@ def test_api_documents_children_create_with_docx_file_disabled(mock_convert, set
parent = factories.DocumentFactory(creator=user, users=[(user, "owner")])
response = client.post(
f"/api/v1.0/documents/{parent.id}/children/",
{
"file": file,
},
format="multipart",
)
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
f"/api/v1.0/documents/{parent.id}/children/",
{
"file": file,
},
format="multipart",
)
assert response.status_code == 400
assert response.json() == {"file": ["file upload is not allowed"]}
@@ -384,6 +393,9 @@ def test_api_documents_children_create_with_docx_file_disabled(mock_convert, set
# Verify the converter was not called
mock_convert.assert_not_called()
# No event should be tracked since the upload is rejected
mock_capture.assert_not_called()
def test_api_documents_children_create_with_file_max_size_exceeded(settings):
"""
@@ -207,12 +207,23 @@ 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(
mock_capture.assert_any_call(
PosthogEventName.DOC_CREATED,
user,
{},
document=document,
)
mock_capture.assert_any_call(
PosthogEventName.DOC_IMPORTED,
user,
{
"content_type": mime_types.MARKDOWN,
"create_for_owner": True,
},
document=document,
)
assert mock_capture.call_count == 2
assert Invitation.objects.exists() is False
@@ -262,12 +273,23 @@ 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(
mock_capture.assert_any_call(
PosthogEventName.DOC_CREATED,
None,
{},
document=document,
)
mock_capture.assert_any_call(
PosthogEventName.DOC_IMPORTED,
None,
{
"content_type": mime_types.MARKDOWN,
"create_for_owner": True,
},
document=document,
)
assert mock_capture.call_count == 2
invitation = Invitation.objects.get()
assert invitation.email == "john.doe@example.com"
@@ -16,6 +16,7 @@ from core.services.converter_services import (
ConversionError,
ServiceUnavailableError,
)
from core.utils.analytics import PosthogEventName
pytestmark = pytest.mark.django_db
@@ -60,13 +61,14 @@ def test_api_documents_create_with_docx_file_success(mock_convert, settings):
file = BytesIO(file_content)
file.name = "My Important Document.docx"
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
with patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
assert response.status_code == 201
document = Document.objects.get()
@@ -81,6 +83,21 @@ def test_api_documents_create_with_docx_file_success(mock_convert, settings):
accept=mime_types.YJS,
)
# The successful conversion should be tracked in PostHog
mock_capture.assert_any_call(
PosthogEventName.DOC_IMPORTED,
user,
{"content_type": mime_types.DOCX},
)
mock_capture.assert_any_call(
PosthogEventName.DOC_CREATED,
user,
{},
document=document,
)
assert mock_capture.call_count == 2
@patch("core.services.converter_services.Converter.convert")
def test_api_documents_create_with_docx_file_disabled(mock_convert, settings):
@@ -98,13 +115,14 @@ def test_api_documents_create_with_docx_file_disabled(mock_convert, settings):
file = BytesIO(file_content)
file.name = "My Important Document.docx"
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
with patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
assert response.status_code == 400
assert response.json() == {"file": ["file upload is not allowed"]}
@@ -112,6 +130,9 @@ def test_api_documents_create_with_docx_file_disabled(mock_convert, settings):
# Verify the converter was not called
mock_convert.assert_not_called()
# No event should be tracked since the upload is rejected
mock_capture.assert_not_called()
@patch("core.services.converter_services.Converter.convert")
def test_api_documents_create_with_markdown_file_success(mock_convert, settings):
@@ -133,13 +154,14 @@ def test_api_documents_create_with_markdown_file_success(mock_convert, settings)
file = BytesIO(file_content)
file.name = "readme.md"
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
with patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
assert response.status_code == 201
document = Document.objects.get()
@@ -154,6 +176,21 @@ def test_api_documents_create_with_markdown_file_success(mock_convert, settings)
accept=mime_types.YJS,
)
# The successful conversion should be tracked in PostHog
mock_capture.assert_any_call(
PosthogEventName.DOC_IMPORTED,
user,
{"content_type": mime_types.MARKDOWN},
)
mock_capture.assert_any_call(
PosthogEventName.DOC_CREATED,
user,
{},
document=document,
)
assert mock_capture.call_count == 2
@patch("core.services.converter_services.Converter.convert")
def test_api_documents_create_with_file_and_explicit_title(mock_convert, settings):
@@ -175,20 +212,36 @@ def test_api_documents_create_with_file_and_explicit_title(mock_convert, setting
file = BytesIO(file_content)
file.name = "Uploaded Document.docx"
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
"title": "This should be overridden",
},
format="multipart",
)
with patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
"title": "This should be overridden",
},
format="multipart",
)
assert response.status_code == 201
document = Document.objects.get()
# The filename should take precedence
assert document.title == "Uploaded Document.docx"
# The successful conversion should be tracked in PostHog
mock_capture.assert_any_call(
PosthogEventName.DOC_IMPORTED,
user,
{"content_type": mime_types.DOCX},
)
mock_capture.assert_any_call(
PosthogEventName.DOC_CREATED,
user,
{},
document=document,
)
assert mock_capture.call_count == 2
def test_api_documents_create_with_empty_file(settings):
"""
@@ -204,18 +257,21 @@ def test_api_documents_create_with_empty_file(settings):
file = BytesIO(b"")
file.name = "empty.docx"
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
with patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
assert response.status_code == 400
assert response.json() == {"file": ["The submitted file is empty."]}
assert not Document.objects.exists()
mock_capture.assert_not_called()
@patch("core.services.converter_services.Converter.convert")
def test_api_documents_create_with_file_conversion_error(mock_convert, settings):
@@ -236,18 +292,22 @@ def test_api_documents_create_with_file_conversion_error(mock_convert, settings)
file = BytesIO(file_content)
file.name = "corrupted.docx"
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
with patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
assert response.status_code == 400
assert response.json() == {"file": ["Could not convert file content"]}
assert not Document.objects.exists()
# No event should be tracked when the conversion fails
mock_capture.assert_not_called()
@patch("core.services.converter_services.Converter.convert")
def test_api_documents_create_with_file_service_unavailable(mock_convert, settings):
@@ -270,18 +330,22 @@ def test_api_documents_create_with_file_service_unavailable(mock_convert, settin
file = BytesIO(file_content)
file.name = "document.docx"
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
with patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
assert response.status_code == 400
assert response.json() == {"file": ["Could not convert file content"]}
assert not Document.objects.exists()
# No event should be tracked when the conversion service is unavailable
mock_capture.assert_not_called()
def test_api_documents_create_without_file_still_works():
"""
@@ -291,13 +355,14 @@ def test_api_documents_create_without_file_still_works():
client = APIClient()
client.force_login(user)
response = client.post(
"/api/v1.0/documents/",
{
"title": "Regular document without file",
},
format="json",
)
with patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
"/api/v1.0/documents/",
{
"title": "Regular document without file",
},
format="json",
)
assert response.status_code == 201
document = Document.objects.get()
@@ -305,6 +370,13 @@ def test_api_documents_create_without_file_still_works():
assert document.content is None
assert document.accesses.filter(role="owner", user=user).exists()
mock_capture.assert_called_once_with(
PosthogEventName.DOC_CREATED,
user,
{},
document=document,
)
@patch("core.services.converter_services.Converter.convert")
def test_api_documents_create_with_file_null_value(mock_convert, settings):
@@ -317,20 +389,27 @@ def test_api_documents_create_with_file_null_value(mock_convert, settings):
settings.CONVERSION_UPLOAD_ENABLED = True
response = client.post(
"/api/v1.0/documents/",
{
"title": "Document with null file",
"file": None,
},
format="json",
)
with patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
"/api/v1.0/documents/",
{
"title": "Document with null file",
"file": None,
},
format="json",
)
assert response.status_code == 201
document = Document.objects.get()
assert document.title == "Document with null file"
# Converter should not have been called
mock_convert.assert_not_called()
mock_capture.assert_called_once_with(
PosthogEventName.DOC_CREATED,
user,
{},
document=document,
)
@patch("core.services.converter_services.Converter.convert")
@@ -355,13 +434,14 @@ def test_api_documents_create_with_file_preserves_content_format(
file = BytesIO(file_content)
file.name = "complex_document.docx"
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
with patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
assert response.status_code == 201
document = Document.objects.get()
@@ -369,6 +449,21 @@ def test_api_documents_create_with_file_preserves_content_format(
# Verify the content is stored as returned by the converter
assert document.content == converted_yjs
# The successful conversion should be tracked in PostHog
mock_capture.assert_any_call(
PosthogEventName.DOC_IMPORTED,
user,
{"content_type": mime_types.DOCX},
)
mock_capture.assert_any_call(
PosthogEventName.DOC_CREATED,
user,
{},
document=document,
)
assert mock_capture.call_count == 2
# Verify it's valid base64 (can be decoded)
try:
b64decode(converted_yjs)
@@ -396,18 +491,34 @@ def test_api_documents_create_with_file_unicode_filename(mock_convert, settings)
file = BytesIO(file_content)
file.name = "文档-télécharger-документ.docx"
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
with patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
assert response.status_code == 201
document = Document.objects.get()
assert document.title == "文档-télécharger-документ.docx"
# The successful conversion should be tracked in PostHog
mock_capture.assert_any_call(
PosthogEventName.DOC_IMPORTED,
user,
{"content_type": mime_types.DOCX},
)
mock_capture.assert_any_call(
PosthogEventName.DOC_CREATED,
user,
{},
document=document,
)
assert mock_capture.call_count == 2
def test_api_documents_create_with_file_max_size_exceeded(settings):
"""
@@ -423,17 +534,19 @@ def test_api_documents_create_with_file_max_size_exceeded(settings):
file = BytesIO(b"a" * (10))
file.name = "test.docx"
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
with patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
assert response.status_code == 400
assert response.json() == {"file": ["File size exceeds the maximum limit of 0 MB."]}
mock_capture.assert_not_called()
def test_api_documents_create_with_file_extension_not_allowed(settings):
@@ -450,13 +563,14 @@ def test_api_documents_create_with_file_extension_not_allowed(settings):
file = BytesIO(b"fake docx content")
file.name = "test.md"
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
with patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
"/api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
assert response.status_code == 400
assert response.json() == {
@@ -464,3 +578,5 @@ def test_api_documents_create_with_file_extension_not_allowed(settings):
"File extension .md is not allowed. Allowed extensions are: ['.docx']."
]
}
mock_capture.assert_not_called()
@@ -18,6 +18,7 @@ from rest_framework.test import APIClient
from core import factories, models
from core.services import mime_types
from core.utils.analytics import PosthogEventName
pytestmark = pytest.mark.django_db
@@ -283,13 +284,14 @@ def test_external_api_documents_create_with_markdown_file_success(
file = BytesIO(file_content)
file.name = "readme.md"
response = client.post(
"/external_api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
with patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
"/external_api/v1.0/documents/",
{
"file": file,
},
format="multipart",
)
assert response.status_code == 201
@@ -307,6 +309,13 @@ def test_external_api_documents_create_with_markdown_file_success(
accept=mime_types.YJS,
)
# The successful conversion should be tracked in PostHog
mock_capture.assert_any_call(
PosthogEventName.DOC_IMPORTED,
user_specific_sub,
{"content_type": mime_types.MARKDOWN},
)
def test_external_api_documents_list_with_multiple_roles(
user_token, resource_server_backend, user_specific_sub
+1
View File
@@ -17,6 +17,7 @@ class PosthogEventName(StrEnum):
DOC_CREATED = "doc_created"
DOC_DELETED = "doc_deleted"
DOC_DUPLICATED = "doc_duplicated"
DOC_IMPORTED = "doc_imported"
USER_LOGIN = "user_login"