diff --git a/analytics/views.py b/analytics/views.py index f428684..1641648 100644 --- a/analytics/views.py +++ b/analytics/views.py @@ -88,6 +88,9 @@ def weekly_analytics(request): campaigns = Campaign.objects.filter(user=request.user, id=campaign_id) else: campaigns = Campaign.objects.filter(user=request.user) + + selected_campaign = campaigns.first() if campaign_id else None + subscriber_list = selected_campaign.subscriber_list if selected_campaign else None # Calculate date range for the past 7 days end_date = timezone.now() @@ -107,6 +110,7 @@ def weekly_analytics(request): 'delivery_rate': 0, 'open_rate': 0, 'click_rate': 0, + 'subscriber_count': 0, } # Get all email events for user's campaigns in the past 7 days @@ -151,6 +155,13 @@ def weekly_analytics(request): if delivered > 0: data['open_rate'] = round((opened / delivered) * 100, 2) data['click_rate'] = round((clicked / delivered) * 100, 2) + + if subscriber_list: + date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() + day_end = timezone.make_aware(datetime.combine(date_obj, datetime.max.time())) + data['subscriber_count'] = subscriber_list.subscribers.filter( + created_at__lte=day_end + ).count() # Convert to sorted list result = sorted(daily_data.values(), key=lambda x: x['date']) @@ -293,7 +304,7 @@ def track_open(request, tracking_id, encoded_email): if subscriber_email: # Always process synchronously (no Celery) try: - from campaigns.models import EmailEvent, Campaign + from campaigns.models import EmailEvent # Find the sent event with the same tracking ID sent_event = EmailEvent.objects.get( id=tracking_id, @@ -316,11 +327,6 @@ def track_open(request, tracking_id, encoded_email): event_type='opened' ) - # Update campaign metrics - campaign = sent_event.email.campaign - campaign.open_count += 1 - campaign.save(update_fields=['open_count']) - logger.info(f"Recorded open event for {subscriber_email}") else: logger.debug(f"Open event already exists for {subscriber_email}, skipping duplicate") @@ -349,7 +355,7 @@ def message_split_gif(request): if subscriber_email: # Always process synchronously (no Celery) try: - from campaigns.models import EmailEvent, Campaign + from campaigns.models import EmailEvent # Find the sent event with the same tracking ID sent_event = EmailEvent.objects.get( id=tracking_id, @@ -372,11 +378,6 @@ def message_split_gif(request): event_type='opened' ) - # Update campaign metrics - campaign = sent_event.email.campaign - campaign.open_count += 1 - campaign.save(update_fields=['open_count']) - logger.info(f"Recorded open event for {subscriber_email}") else: logger.debug(f"Open event already exists for {subscriber_email}, skipping duplicate") @@ -415,7 +416,7 @@ def track_click(request, tracking_id): from django.conf import settings if sys.platform == 'win32' and settings.DEBUG: # Process synchronously - from campaigns.models import EmailEvent, Campaign + from campaigns.models import EmailEvent try: sent_event = EmailEvent.objects.get( id=tracking_id, @@ -430,11 +431,6 @@ def track_click(request, tracking_id): event_type='clicked', link_clicked=destination_url ) - - # Update campaign metrics - campaign = sent_event.email.campaign - campaign.click_count += 1 - campaign.save(update_fields=['click_count']) except EmailEvent.DoesNotExist: pass except Exception: diff --git a/campaigns/tasks.py b/campaigns/tasks.py index 791cdd8..7ad3712 100644 --- a/campaigns/tasks.py +++ b/campaigns/tasks.py @@ -1218,7 +1218,7 @@ def _safe_replacement(val): def process_email_open(tracking_id, subscriber_email): """Process email open event.""" - from .models import EmailEvent, Campaign + from .models import EmailEvent try: # Find the sent event with the same tracking ID @@ -1243,11 +1243,6 @@ def process_email_open(tracking_id, subscriber_email): event_type='opened' ) - # Update campaign metrics - campaign = sent_event.email.campaign - campaign.open_count += 1 - campaign.save(update_fields=['open_count']) - logger.info(f"Recorded open event for {subscriber_email}") else: logger.debug(f"Open event already exists for {subscriber_email}, skipping duplicate") @@ -1260,7 +1255,7 @@ def process_email_open(tracking_id, subscriber_email): def process_email_click(tracking_id, subscriber_email, link_url): """Process email click event.""" - from .models import EmailEvent, Campaign + from .models import EmailEvent try: # Find the sent event with the same tracking ID @@ -1278,11 +1273,6 @@ def process_email_click(tracking_id, subscriber_email, link_url): link_clicked=link_url ) - # Update campaign metrics (count each click) - campaign = sent_event.email.campaign - campaign.click_count += 1 - campaign.save(update_fields=['click_count']) - logger.info(f"Recorded click event for {subscriber_email} on {link_url}") except EmailEvent.DoesNotExist: diff --git a/campaigns/views.py b/campaigns/views.py index f88ecc3..528a809 100644 --- a/campaigns/views.py +++ b/campaigns/views.py @@ -876,6 +876,14 @@ def campaign_analysis_view(request): elif ev.event_type == 'complained': by_month[month]['complaints'] += 1 + # Keep top metric cards in sync with real-time event data + campaign.sent_count = events.filter(event_type='sent').count() + campaign.open_count = events.filter(event_type='opened').count() + campaign.click_count = events.filter(event_type='clicked').count() + campaign.bounce_count = events.filter(event_type='bounced').count() + campaign.unsubscribe_count = events.filter(event_type='unsubscribed').count() + campaign.complaint_count = events.filter(event_type='complained').count() + # Per-email breakdown with unique opens for e in campaign.emails.all(): sent = EmailEvent.objects.filter(email=e, event_type='sent').count() @@ -941,10 +949,15 @@ def campaign_stats_api(request, campaign_id): event_type='complained' ).count() - # Get opens and clicks from campaign model (these are incremented via signals) - campaign.refresh_from_db() - open_count = campaign.open_count - click_count = campaign.click_count + # Get opens and clicks from events for consistency across all event sources + open_count = EmailEvent.objects.filter( + email__campaign=campaign, + event_type='opened' + ).count() + click_count = EmailEvent.objects.filter( + email__campaign=campaign, + event_type='clicked' + ).count() # Calculate rates delivered_count = sent_count - bounce_count diff --git a/docs/press/15. Subject - {{first_name}} {{last_name}} story idea on DripEmails.org.txt b/docs/press/15. Subject - {{first_name}} {{last_name}} story idea on DripEmails.org.txt index 00d6184..34ad50c 100644 --- a/docs/press/15. Subject - {{first_name}} {{last_name}} story idea on DripEmails.org.txt +++ b/docs/press/15. Subject - {{first_name}} {{last_name}} story idea on DripEmails.org.txt @@ -17,7 +17,7 @@ What DripEmails.org offers: - Gmail + IMAP auto-engagement workflows - Multi-step drip campaigns with flexible timing - AI-assisted drafting and revision for campaign emails -- Personalized templates with merge fields like {{first_name}}, {{last_name}}, and {{email}} +- Personalized templates with merge fields like {{first_name}} and {{email}} - Campaign analytics (opens, clicks, and engagement tracking) If useful, I can share: diff --git a/docs/press/16. Subject - {{first_name}} {{last_name}} covering smarter email follow-up at DripEmails.org.txt b/docs/press/16. Subject - {{first_name}} {{last_name}} covering smarter email follow-up at DripEmails.org.txt index fec8bca..7259627 100644 --- a/docs/press/16. Subject - {{first_name}} {{last_name}} covering smarter email follow-up at DripEmails.org.txt +++ b/docs/press/16. Subject - {{first_name}} {{last_name}} covering smarter email follow-up at DripEmails.org.txt @@ -17,7 +17,7 @@ Product highlights: - Gmail + IMAP integration for inbox-aware automation - Configurable multi-step drip sequences - AI-assisted writing and message revision -- Merge-field personalization including {{first_name}}, {{last_name}}, and {{email}} +- Merge-field personalization including {{first_name}}, and {{email}} - Engagement analytics for optimization over time Happy to provide anything useful for coverage: diff --git a/templates/campaigns/campaign_analysis.html b/templates/campaigns/campaign_analysis.html index 4eee98a..15cb8aa 100644 --- a/templates/campaigns/campaign_analysis.html +++ b/templates/campaigns/campaign_analysis.html @@ -90,7 +90,7 @@
{% trans "No subscribers found for this campaign" %}
+