diff --git a/posthog/celery.py b/posthog/celery.py index 96095e18b587f..0ab9fb52f00d9 100644 --- a/posthog/celery.py +++ b/posthog/celery.py @@ -61,9 +61,6 @@ def setup_periodic_tasks(sender: Celery, **kwargs): if getattr(settings, "MULTI_TENANCY", False): sender.add_periodic_task(crontab(hour=0, minute=0), calculate_billing_daily_usage.s()) # every day midnight UTC - # Send weekly email report (~ 8:00 SF / 16:00 UK / 17:00 EU) - sender.add_periodic_task(crontab(day_of_week="mon", hour=15, minute=0), send_weekly_email_report.s()) - sender.add_periodic_task(crontab(day_of_week="fri", hour=0, minute=0), clean_stale_partials.s()) # delete old plugin logs every 4 hours @@ -332,14 +329,6 @@ def calculate_cohort_ids_in_feature_flags_task(): calculate_cohort_ids_in_feature_flags() -@app.task(ignore_result=True) -def send_weekly_email_report(): - if settings.EMAIL_REPORTS_ENABLED: - from posthog.tasks.email import send_weekly_email_reports - - send_weekly_email_reports() - - @app.task(ignore_result=True, bind=True) def debug_task(self): print(f"Request: {self.request!r}") diff --git a/posthog/settings/__init__.py b/posthog/settings/__init__.py index 16b6df9d610b8..4651f944103f9 100644 --- a/posthog/settings/__init__.py +++ b/posthog/settings/__init__.py @@ -117,6 +117,3 @@ # Lastly, cloud settings override and modify all from posthog.settings.cloud import * - -# TODO: Temporary -EMAIL_REPORTS_ENABLED: bool = get_from_env("EMAIL_REPORTS_ENABLED", False, type_cast=str_to_bool) diff --git a/posthog/tasks/email.py b/posthog/tasks/email.py index b4c924d5b191f..8d0e8639f657e 100644 --- a/posthog/tasks/email.py +++ b/posthog/tasks/email.py @@ -1,33 +1,15 @@ -import datetime -import logging import uuid -from typing import Optional import structlog from django.conf import settings from posthog.celery import app -from posthog.email import EmailMessage, is_email_available -from posthog.models import Event, Organization, OrganizationInvite, PersonDistinctId, Team, User -from posthog.templatetags.posthog_filters import compact_number -from posthog.utils import get_previous_week +from posthog.email import EmailMessage +from posthog.models import Organization, OrganizationInvite, User logger = structlog.get_logger(__name__) -def send_weekly_email_reports() -> None: - """ - Schedules an async task to send the weekly email report for each team. - """ - - if not is_email_available(): - logger.info("Skipping send_weekly_email_report because email is not properly configured") - return - - for team in Team.objects.order_by("pk"): - _send_weekly_email_report_for_team.delay(team_id=team.pk) - - def send_message_to_all_staff_users(message: EmailMessage) -> None: for user in User.objects.filter(is_staff=True): message.add_recipient(email=user.email, name=user.first_name) @@ -35,92 +17,6 @@ def send_message_to_all_staff_users(message: EmailMessage) -> None: message.send() -@app.task(ignore_result=True, max_retries=1) -def _send_weekly_email_report_for_team(team_id: int) -> None: - """ - Sends the weekly email report to all users in a team. - """ - - period_start, period_end = get_previous_week() - last_week_start: datetime.datetime = period_start - datetime.timedelta(7) - last_week_end: datetime.datetime = period_end - datetime.timedelta(7) - - campaign_key: str = f"weekly_report_for_team_{team_id}_on_{period_start.strftime('%Y-%m-%d')}" - - team = Team.objects.get(pk=team_id) - - event_data_set = Event.objects.filter(team=team, timestamp__gte=period_start, timestamp__lte=period_end,) - - active_users = PersonDistinctId.objects.filter( - distinct_id__in=event_data_set.values("distinct_id").distinct(), - ).distinct() - active_users_count: int = active_users.count() - - if active_users_count == 0: - # TODO: Send an email prompting fix to no active users - return - - last_week_users = PersonDistinctId.objects.filter( - distinct_id__in=Event.objects.filter(team=team, timestamp__gte=last_week_start, timestamp__lte=last_week_end,) - .values("distinct_id") - .distinct(), - ).distinct() - last_week_users_count: int = last_week_users.count() - - two_weeks_ago_users = PersonDistinctId.objects.filter( - distinct_id__in=Event.objects.filter( - team=team, - timestamp__gte=last_week_start - datetime.timedelta(7), - timestamp__lte=last_week_end - datetime.timedelta(7), - ) - .values("distinct_id") - .distinct(), - ).distinct() # used to compute delta in churned users - two_weeks_ago_users_count: int = two_weeks_ago_users.count() - - not_last_week_users = PersonDistinctId.objects.filter( - pk__in=active_users.difference(last_week_users,).values_list("pk", flat=True,) - ) # users that were present this week but not last week - - churned_count = last_week_users.difference(active_users).count() - churned_ratio: Optional[float] = (churned_count / last_week_users_count if last_week_users_count > 0 else None) - last_week_churn_ratio: Optional[float] = ( - two_weeks_ago_users.difference(last_week_users).count() / two_weeks_ago_users_count - if two_weeks_ago_users_count > 0 - else None - ) - churned_delta: Optional[float] = ( - churned_ratio / last_week_churn_ratio - 1 if last_week_churn_ratio else None # type: ignore - ) - - message = EmailMessage( - campaign_key=campaign_key, - subject=f"PostHog weekly report for {period_start.strftime('%b %d, %Y')} to {period_end.strftime('%b %d')}", - template_name="weekly_report", - template_context={ - "preheader": f"Your PostHog weekly report is ready! Your team had {compact_number(active_users_count)} active users last week! 🎉", - "team": team.name, - "period_start": period_start, - "period_end": period_end, - "active_users": active_users_count, - "active_users_delta": active_users_count / last_week_users_count - 1 if last_week_users_count > 0 else None, - "user_distribution": { - "new": not_last_week_users.filter(person__created_at__gte=period_start).count() / active_users_count, - "retained": active_users.intersection(last_week_users).count() / active_users_count, - "resurrected": not_last_week_users.filter(person__created_at__lt=period_start).count() - / active_users_count, - }, - "churned_users": {"abs": churned_count, "ratio": churned_ratio, "delta": churned_delta}, - }, - ) - - for user in team.organization.members.all(): - # TODO: Skip "unsubscribed" users - message.add_recipient(email=user.email, name=user.first_name) - - message.send() - - @app.task(max_retries=1) def send_invite(invite_id: str) -> None: campaign_key: str = f"invite_email_{invite_id}" diff --git a/posthog/test/test_email.py b/posthog/test/test_email.py index b41682e343386..1d4533f26da19 100644 --- a/posthog/test/test_email.py +++ b/posthog/test/test_email.py @@ -1,19 +1,12 @@ -import datetime -import hashlib -from typing import List -from unittest.mock import patch - import pytz from constance.test import override_config -from django.conf import settings from django.core import mail from django.core.exceptions import ImproperlyConfigured from django.utils import timezone from freezegun import freeze_time from posthog.email import EmailMessage, _send_email -from posthog.models import Event, MessagingRecord, Organization, Person, Team, User -from posthog.tasks.email import send_weekly_email_reports +from posthog.models import MessagingRecord, Organization, Person, Team, User from posthog.test.base import BaseTest @@ -41,34 +34,6 @@ def setUp(self): defaults={"sent_at": timezone.now()}, ) # This user should not get the emails - last_week = datetime.datetime(2020, 9, 17, 3, 22, tzinfo=pytz.UTC) - two_weeks_ago = datetime.datetime(2020, 9, 8, 19, 54, tzinfo=pytz.UTC) - - self.persons: List = [self.create_person(self.team, str(i)) for i in range(0, 7)] - - # Resurrected - self.persons[0].created_at = timezone.now() - datetime.timedelta(weeks=3) - self.persons[0].save() - self.persons[1].created_at = timezone.now() - datetime.timedelta(weeks=4) - self.persons[1].save() - Event.objects.create(team=self.team, timestamp=last_week, distinct_id=0) - Event.objects.create(team=self.team, timestamp=last_week, distinct_id=1) - - # Retained - Event.objects.create(team=self.team, timestamp=last_week, distinct_id=2) - Event.objects.create(team=self.team, timestamp=two_weeks_ago, distinct_id=2) - Event.objects.create(team=self.team, timestamp=last_week, distinct_id=3) - Event.objects.create(team=self.team, timestamp=two_weeks_ago, distinct_id=3) - Event.objects.create(team=self.team, timestamp=last_week, distinct_id=4) - Event.objects.create(team=self.team, timestamp=two_weeks_ago, distinct_id=4) - - # New - Event.objects.create(team=self.team, timestamp=last_week, distinct_id=5) - Event.objects.create(team=self.team, timestamp=last_week, distinct_id=5) - - # Churned - Event.objects.create(team=self.team, timestamp=two_weeks_ago, distinct_id=6) - def test_cant_send_emails_if_not_properly_configured(self) -> None: with override_config(EMAIL_HOST=None): with self.assertRaises(ImproperlyConfigured) as e: @@ -105,84 +70,3 @@ def test_cant_send_same_campaign_twice(self) -> None: record.refresh_from_db() self.assertEqual(record.sent_at, sent_at) - - @freeze_time("2020-09-21") - @override_config(EMAIL_HOST="localhost") - def test_weekly_email_report(self) -> None: - - record_count: int = MessagingRecord.objects.count() - - expected_recipients: List[str] = ["test@posthog.com", "test2@posthog.com"] - - with self.settings(CELERY_TASK_ALWAYS_EAGER=True, SITE_URL="http://localhost:9999"): - send_weekly_email_reports() - - self.assertSetEqual({",".join(outmail.to) for outmail in mail.outbox}, set(expected_recipients)) - - self.assertEqual( - mail.outbox[0].subject, "PostHog weekly report for Sep 14, 2020 to Sep 20", - ) - - self.assertEqual( - mail.outbox[0].body, "", - ) # no plain-text version support yet - - html_message = mail.outbox[0].alternatives[0][0] # type: ignore - self.validate_basic_html( - html_message, - "http://localhost:9999", - preheader="Your PostHog weekly report is ready! Your team had 6 active users last week! 🎉", - ) - - # Ensure records are properly saved to prevent duplicate emails - self.assertEqual(MessagingRecord.objects.count(), record_count + 2) - for email in expected_recipients: - email_hash = hashlib.sha256(f"{settings.SECRET_KEY}_{email}".encode()).hexdigest() - record = MessagingRecord.objects.get( - email_hash=email_hash, campaign_key=f"weekly_report_for_team_{self.team.pk}_on_2020-09-14", - ) - self.assertTrue((timezone.now() - record.sent_at).total_seconds() < 5) - - @patch("posthog.tasks.email.EmailMessage") - @override_config(EMAIL_HOST="localhost") - @freeze_time("2020-09-21") - def test_weekly_email_report_content(self, mock_email_message): - - with self.settings(CELERY_TASK_ALWAYS_EAGER=True): - send_weekly_email_reports() - - self.assertEqual( - mock_email_message.call_args[1]["campaign_key"], f"weekly_report_for_team_{self.team.pk}_on_2020-09-14", - ) # Campaign key - self.assertEqual( - mock_email_message.call_args[1]["subject"], "PostHog weekly report for Sep 14, 2020 to Sep 20", - ) # Email subject - self.assertEqual(mock_email_message.call_args[1]["template_name"], "weekly_report") - - template_context = mock_email_message.call_args[1]["template_context"] - - self.assertEqual(template_context["team"], "The Bakery") - self.assertEqual( - template_context["period_start"], datetime.datetime(2020, 9, 14, tzinfo=pytz.UTC), - ) - self.assertEqual( - template_context["period_end"], datetime.datetime(2020, 9, 20, 23, 59, 59, 999999, tzinfo=pytz.UTC), - ) - self.assertEqual( - template_context["active_users"], 6, - ) - self.assertEqual( - template_context["active_users_delta"], 0.5, - ) - self.assertEqual( - round(template_context["user_distribution"]["new"], 2), 0.17, - ) - self.assertEqual( - template_context["user_distribution"]["retained"], 0.5, - ) - self.assertEqual( - round(template_context["user_distribution"]["resurrected"], 2), 0.33, - ) - self.assertEqual( - template_context["churned_users"], {"abs": 1, "ratio": 0.25, "delta": None}, - )