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 "Campaign Statistics" %

{% trans "Past 7 Days Trends" %}

-
+

{% trans "Delivery Rate" %}

@@ -109,6 +109,18 @@

{% trans "Click-Through Rate" %}

+
+

{% trans "Subscribers" %}

+
+ {% if campaign.subscriber_list %} + + {% else %} +
+

{% trans "No subscribers found for this campaign" %}

+
+ {% endif %} +
+
@@ -200,6 +212,7 @@

{% trans "Per-email breakdo let deliveryChart = null; let openChart = null; let clickChart = null; + let subscriberChart = null; // Wait for DOM to be ready document.addEventListener('DOMContentLoaded', function() { @@ -253,11 +266,12 @@

{% trans "Per-email breakdo const deliveryRates = weeklyData.map(d => d.delivery_rate); const openRates = weeklyData.map(d => d.open_rate); const clickRates = weeklyData.map(d => d.click_rate); + const subscriberCounts = weeklyData.map(d => d.subscriber_count || 0); - console.log('Chart data:', { dates, deliveryRates, openRates, clickRates }); + console.log('Chart data:', { dates, deliveryRates, openRates, clickRates, subscriberCounts }); - // Common chart options - const commonOptions = { + // Common chart options for percentage-based charts + const rateChartOptions = { responsive: true, maintainAspectRatio: false, plugins: { @@ -297,6 +311,45 @@

{% trans "Per-email breakdo } } }; + + // Common chart options for raw subscriber count chart + const subscriberChartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + }, + tooltip: { + mode: 'index', + intersect: false + } + }, + scales: { + y: { + beginAtZero: true, + ticks: { + precision: 0, + font: { + size: 10 + } + }, + grid: { + display: true + } + }, + x: { + ticks: { + font: { + size: 10 + } + }, + grid: { + display: false + } + } + } + }; // Delivery Rate Chart const deliveryRateCtx = document.getElementById('deliveryRateChart'); @@ -320,7 +373,7 @@

{% trans "Per-email breakdo tension: 0.4 }] }, - options: commonOptions + options: rateChartOptions }); console.log('Delivery chart created:', deliveryChart); } else { @@ -349,7 +402,7 @@

{% trans "Per-email breakdo tension: 0.4 }] }, - options: commonOptions + options: rateChartOptions }); console.log('Open chart created:', openChart); } else { @@ -378,12 +431,39 @@

{% trans "Per-email breakdo tension: 0.4 }] }, - options: commonOptions + options: rateChartOptions }); console.log('Click chart created:', clickChart); } else { console.error('Click Rate Canvas not found!'); } + + // Subscriber Trend Chart + const subscriberTrendCtx = document.getElementById('subscriberTrendChart'); + console.log('Subscriber Trend Canvas:', subscriberTrendCtx); + if (subscriberTrendCtx) { + if (subscriberChart) { + subscriberChart.destroy(); + } + console.log('Creating subscriber trend chart...'); + subscriberChart = new Chart(subscriberTrendCtx.getContext('2d'), { + type: 'line', + data: { + labels: dates, + datasets: [{ + label: 'Subscribers', + data: subscriberCounts, + borderColor: 'rgb(99, 102, 241)', + backgroundColor: 'rgba(99, 102, 241, 0.1)', + borderWidth: 2, + fill: true, + tension: 0.4 + }] + }, + options: subscriberChartOptions + }); + console.log('Subscriber chart created:', subscriberChart); + } }) .catch(error => { console.error('Error loading weekly analytics:', error);