2022-06-24 10:29:45 +02:00
|
|
|
from datetime import datetime, timedelta
|
2022-07-04 16:52:50 +02:00
|
|
|
from typing import Optional
|
2022-06-24 10:29:45 +02:00
|
|
|
|
|
|
|
import structlog
|
2024-01-18 14:28:10 +01:00
|
|
|
from celery import shared_task
|
2023-09-29 15:53:33 +02:00
|
|
|
from prometheus_client import Counter
|
2023-10-16 14:51:24 +02:00
|
|
|
from sentry_sdk import capture_exception, capture_message
|
2022-06-24 10:29:45 +02:00
|
|
|
|
|
|
|
from ee.tasks.subscriptions.email_subscriptions import send_email_subscription_report
|
|
|
|
from ee.tasks.subscriptions.slack_subscriptions import send_slack_subscription_report
|
|
|
|
from ee.tasks.subscriptions.subscription_utils import generate_assets
|
2024-01-17 17:38:14 +01:00
|
|
|
from posthog import settings
|
2022-06-24 10:29:45 +02:00
|
|
|
from posthog.models.subscription import Subscription
|
2024-01-18 14:28:10 +01:00
|
|
|
from posthog.tasks.utils import CeleryQueue
|
2022-06-24 10:29:45 +02:00
|
|
|
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
|
|
|
2023-09-29 15:53:33 +02:00
|
|
|
SUBSCRIPTION_QUEUED = Counter(
|
2023-10-26 12:38:15 +02:00
|
|
|
"subscription_queued",
|
|
|
|
"A subscription was queued for delivery",
|
|
|
|
labelnames=["destination"],
|
2023-09-29 15:53:33 +02:00
|
|
|
)
|
|
|
|
SUBSCRIPTION_SUCCESS = Counter(
|
2023-10-26 12:38:15 +02:00
|
|
|
"subscription_send_success",
|
|
|
|
"A subscription was sent successfully",
|
|
|
|
labelnames=["destination"],
|
|
|
|
)
|
|
|
|
SUBSCRIPTION_FAILURE = Counter(
|
|
|
|
"subscription_send_failure",
|
|
|
|
"A subscription failed to send",
|
|
|
|
labelnames=["destination"],
|
2023-09-29 15:53:33 +02:00
|
|
|
)
|
|
|
|
|
2022-06-24 10:29:45 +02:00
|
|
|
|
|
|
|
def _deliver_subscription_report(
|
2023-10-26 12:38:15 +02:00
|
|
|
subscription_id: int,
|
|
|
|
previous_value: Optional[str] = None,
|
|
|
|
invite_message: Optional[str] = None,
|
2022-06-24 10:29:45 +02:00
|
|
|
) -> None:
|
|
|
|
subscription = (
|
|
|
|
Subscription.objects.prefetch_related("dashboard__insights")
|
2022-09-05 14:38:54 +02:00
|
|
|
.select_related("created_by", "insight", "dashboard")
|
2022-06-24 10:29:45 +02:00
|
|
|
.get(pk=subscription_id)
|
|
|
|
)
|
|
|
|
|
2022-06-24 15:18:34 +02:00
|
|
|
is_new_subscription_target = False
|
|
|
|
if previous_value is not None:
|
|
|
|
# If previous_value is set we are triggering a "new" or "invite" message
|
|
|
|
is_new_subscription_target = subscription.target_value != previous_value
|
|
|
|
|
|
|
|
if not is_new_subscription_target:
|
|
|
|
# Same value as before so nothing to do
|
|
|
|
return
|
|
|
|
|
2023-10-16 14:51:24 +02:00
|
|
|
insights, assets = generate_assets(subscription)
|
|
|
|
|
|
|
|
if not assets:
|
2023-10-26 12:38:15 +02:00
|
|
|
capture_message(
|
|
|
|
"No assets are in this subscription",
|
|
|
|
tags={"subscription_id": subscription.id},
|
|
|
|
)
|
2023-10-16 14:51:24 +02:00
|
|
|
return
|
|
|
|
|
2022-06-24 10:29:45 +02:00
|
|
|
if subscription.target_type == "email":
|
2023-09-29 15:53:33 +02:00
|
|
|
SUBSCRIPTION_QUEUED.labels(destination="email").inc()
|
|
|
|
|
2022-06-24 10:29:45 +02:00
|
|
|
# Send emails
|
2022-06-24 15:18:34 +02:00
|
|
|
emails = subscription.target_value.split(",")
|
|
|
|
if is_new_subscription_target:
|
|
|
|
previous_emails = previous_value.split(",") if previous_value else []
|
|
|
|
emails = list(set(emails) - set(previous_emails))
|
2022-06-24 10:29:45 +02:00
|
|
|
|
2022-06-24 15:18:34 +02:00
|
|
|
for email in emails:
|
2022-06-24 10:29:45 +02:00
|
|
|
try:
|
|
|
|
send_email_subscription_report(
|
|
|
|
email,
|
|
|
|
subscription,
|
|
|
|
assets,
|
2022-06-24 15:18:34 +02:00
|
|
|
invite_message=invite_message or "" if is_new_subscription_target else None,
|
2022-06-24 10:29:45 +02:00
|
|
|
total_asset_count=len(insights),
|
|
|
|
)
|
|
|
|
except Exception as e:
|
2023-09-29 15:53:33 +02:00
|
|
|
SUBSCRIPTION_FAILURE.labels(destination="email").inc()
|
2023-10-02 21:30:03 +02:00
|
|
|
logger.error(
|
|
|
|
"sending subscription failed",
|
|
|
|
subscription_id=subscription.id,
|
|
|
|
next_delivery_date=subscription.next_delivery_date,
|
|
|
|
destination=subscription.target_type,
|
|
|
|
exc_info=True,
|
|
|
|
)
|
2022-06-24 10:29:45 +02:00
|
|
|
capture_exception(e)
|
2023-09-29 15:53:33 +02:00
|
|
|
|
|
|
|
SUBSCRIPTION_SUCCESS.labels(destination="email").inc()
|
2022-06-24 10:29:45 +02:00
|
|
|
|
|
|
|
elif subscription.target_type == "slack":
|
2023-09-29 15:53:33 +02:00
|
|
|
SUBSCRIPTION_QUEUED.labels(destination="slack").inc()
|
|
|
|
|
2022-06-24 10:29:45 +02:00
|
|
|
try:
|
|
|
|
send_slack_subscription_report(
|
2023-10-26 12:38:15 +02:00
|
|
|
subscription,
|
|
|
|
assets,
|
|
|
|
total_asset_count=len(insights),
|
|
|
|
is_new_subscription=is_new_subscription_target,
|
2022-06-24 10:29:45 +02:00
|
|
|
)
|
2023-09-29 15:53:33 +02:00
|
|
|
SUBSCRIPTION_SUCCESS.labels(destination="slack").inc()
|
2022-06-24 10:29:45 +02:00
|
|
|
except Exception as e:
|
2023-09-29 15:53:33 +02:00
|
|
|
SUBSCRIPTION_FAILURE.labels(destination="slack").inc()
|
2023-10-02 21:30:03 +02:00
|
|
|
logger.error(
|
|
|
|
"sending subscription failed",
|
|
|
|
subscription_id=subscription.id,
|
|
|
|
next_delivery_date=subscription.next_delivery_date,
|
|
|
|
destination=subscription.target_type,
|
|
|
|
exc_info=True,
|
|
|
|
)
|
2023-09-29 15:53:33 +02:00
|
|
|
capture_exception(e)
|
2022-06-24 10:29:45 +02:00
|
|
|
else:
|
|
|
|
raise NotImplementedError(f"{subscription.target_type} is not supported")
|
|
|
|
|
2022-06-24 15:18:34 +02:00
|
|
|
if not is_new_subscription_target:
|
2024-04-18 19:38:57 +02:00
|
|
|
subscription.set_next_delivery_date(subscription.next_delivery_date)
|
|
|
|
subscription.save(update_fields=["next_delivery_date"])
|
2022-06-24 10:29:45 +02:00
|
|
|
|
|
|
|
|
2024-01-31 09:49:14 +01:00
|
|
|
@shared_task(queue=CeleryQueue.SUBSCRIPTION_DELIVERY.value)
|
2022-06-24 10:29:45 +02:00
|
|
|
def schedule_all_subscriptions() -> None:
|
|
|
|
"""
|
|
|
|
Schedule all past notifications (with a buffer) to be delivered
|
|
|
|
NOTE: This task is scheduled hourly just before the hour allowing for the 15 minute timedelta to cover
|
|
|
|
all upcoming hourly scheduled subscriptions
|
|
|
|
"""
|
|
|
|
now_with_buffer = datetime.utcnow() + timedelta(minutes=15)
|
2022-08-19 09:49:23 +02:00
|
|
|
subscriptions = (
|
|
|
|
Subscription.objects.filter(next_delivery_date__lte=now_with_buffer, deleted=False)
|
|
|
|
.exclude(dashboard__deleted=True)
|
|
|
|
.exclude(insight__deleted=True)
|
|
|
|
.all()
|
|
|
|
)
|
2022-06-24 10:29:45 +02:00
|
|
|
|
|
|
|
for subscription in subscriptions:
|
2023-10-02 21:30:03 +02:00
|
|
|
logger.info(
|
|
|
|
"Scheduling subscription",
|
|
|
|
subscription_id=subscription.id,
|
|
|
|
next_delivery_date=subscription.next_delivery_date,
|
|
|
|
destination=subscription.target_type,
|
|
|
|
)
|
2022-06-24 10:29:45 +02:00
|
|
|
deliver_subscription_report.delay(subscription.id)
|
|
|
|
|
|
|
|
|
2024-01-17 17:38:14 +01:00
|
|
|
report_timeout_seconds = settings.PARALLEL_ASSET_GENERATION_MAX_TIMEOUT_MINUTES * 60 * 1.5
|
|
|
|
|
|
|
|
|
2024-01-18 14:28:10 +01:00
|
|
|
@shared_task(
|
|
|
|
soft_time_limit=report_timeout_seconds,
|
|
|
|
time_limit=report_timeout_seconds + 10,
|
2024-01-31 09:49:14 +01:00
|
|
|
queue=CeleryQueue.SUBSCRIPTION_DELIVERY.value,
|
2024-01-18 14:28:10 +01:00
|
|
|
)
|
2022-06-24 10:29:45 +02:00
|
|
|
def deliver_subscription_report(subscription_id: int) -> None:
|
|
|
|
return _deliver_subscription_report(subscription_id)
|
|
|
|
|
|
|
|
|
2024-01-18 14:28:10 +01:00
|
|
|
@shared_task(
|
|
|
|
soft_time_limit=report_timeout_seconds,
|
|
|
|
time_limit=report_timeout_seconds + 10,
|
2024-01-31 09:49:14 +01:00
|
|
|
queue=CeleryQueue.SUBSCRIPTION_DELIVERY.value,
|
2024-01-18 14:28:10 +01:00
|
|
|
)
|
2022-06-24 15:18:34 +02:00
|
|
|
def handle_subscription_value_change(
|
|
|
|
subscription_id: int, previous_value: str, invite_message: Optional[str] = None
|
|
|
|
) -> None:
|
|
|
|
return _deliver_subscription_report(subscription_id, previous_value, invite_message)
|