Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from typing import Any
from uuid import UUID

import structlog
import posthoganalytics
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from rest_framework import exceptions, serializers, viewsets
Expand All @@ -12,12 +14,15 @@

from posthog.api.routing import TeamAndOrgViewSetMixin
from posthog.auth import ProjectSecretAPIKeyAuthentication
from posthog.event_usage import groups
from posthog.permissions import is_authenticated_via_project_secret_api_key
from posthog.rate_limit import PersonalOrProjectSecretApiKeyRateThrottle, ProjectSecretApiKeyTeamRateThrottle

from products.messaging.backend.facade.transactional_email import EmailDeliveryStatus, get_email_status
from products.messaging.backend.facade.transactional_email import EmailDeliveryStatus, EmailStatus, get_email_status
from products.messaging.backend.presentation.views.message_templates import require_transactional_email_api

logger = structlog.get_logger(__name__)

MESSAGE_ID_PARAMETER = OpenApiParameter(
name="id",
type=OpenApiTypes.UUID,
Expand Down Expand Up @@ -108,11 +113,35 @@ def retrieve(self, request: Request, *args: Any, pk: str | None = None, **kwargs
if not is_authenticated_via_project_secret_api_key(request):
raise exceptions.NotAuthenticated()
require_transactional_email_api(self.team)
try:
message_id = UUID(str(pk))
except ValueError:
raise NotFound()
email_status = get_email_status(self.team_id, message_id)
email_status = self._email_status(pk)
self._report_status_read(request, email_status)
if email_status is None:
raise NotFound()
return Response(EmailStatusSerializer(email_status).data)

def _email_status(self, pk: str | None) -> EmailStatus | None:
try:
message_id = UUID(str(pk))
except ValueError:
return None
return get_email_status(self.team_id, message_id)

def _report_status_read(self, request: Request, email_status: EmailStatus | None) -> None:
# report_user_action drops the synthetic user behind a project secret API key, so capture directly. That
# user is not a Django User, so its distinct_id is read without a type.
distinct_id = getattr(request.user, "distinct_id", None)
if not isinstance(distinct_id, str):
return
try:
posthoganalytics.capture(
distinct_id=distinct_id,
event="transactional email api status read",
properties={
"team_id": self.team_id,
"outcome": "not_found" if email_status is None else "found",
"status": email_status.status if email_status else None,
},
groups=groups(self.organization, self.team),
)
except Exception as e:
logger.warning("Failed to capture transactional email status read event", error=str(e))
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,17 @@ def _status_rows(self, rows: list[tuple[str, int, str]], team: Team | None = Non
def test_reports_the_latest_status_of_the_send(self, _flag, _name, rows, expected_status, status_offset, reason):
self._status_rows(rows)

response = self.client.get(self._url(self.message_id))
with patch(
"products.messaging.backend.presentation.views.transactional_email_status.posthoganalytics.capture"
) as capture:
response = self.client.get(self._url(self.message_id))

assert response.status_code == status.HTTP_200_OK, response.json()
assert capture.call_args.kwargs["properties"] == {
"team_id": self.team.id,
"outcome": "found",
"status": expected_status,
}
assert response.json() == {
"id": self.message_id,
"status": expected_status,
Expand All @@ -137,9 +145,13 @@ def test_answers_404_for_an_email_it_cannot_report(self, _flag, _name, message_i
self._status_rows([("sent", 2, "")], team=Team.objects.create(organization=self.organization, name="Other"))
ids = {"message_id": self.message_id, "old": self._message_id(datetime.now(UTC) - timedelta(days=31))}

response = self.client.get(self._url(ids.get(message_id, message_id)))
with patch(
"products.messaging.backend.presentation.views.transactional_email_status.posthoganalytics.capture"
) as capture:
response = self.client.get(self._url(ids.get(message_id, message_id)))

assert response.status_code == status.HTTP_404_NOT_FOUND
assert capture.call_args.kwargs["properties"]["outcome"] == "not_found"

@parameterized.expand(
[
Expand Down
Loading