Skip to content
Open
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
32 changes: 14 additions & 18 deletions analytics/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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'])
Expand Down Expand Up @@ -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,
Expand All @@ -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")
Expand Down Expand Up @@ -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,
Expand All @@ -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")
Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand Down
14 changes: 2 additions & 12 deletions campaigns/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand All @@ -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
Expand All @@ -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:
Expand Down
21 changes: 17 additions & 4 deletions campaigns/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
94 changes: 87 additions & 7 deletions templates/campaigns/campaign_analysis.html
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ <h2 class="text-lg font-semibold text-gray-900">{% trans "Campaign Statistics" %
<div class="mb-6">
<h2 class="text-lg font-medium mb-2">{% trans "Past 7 Days Trends" %}</h2>
<!-- Rate charts with data -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="p-4 bg-white rounded border">
<h3 class="text-sm text-gray-600 mb-2">{% trans "Delivery Rate" %}</h3>
<div style="height: 150px; position: relative;">
Expand All @@ -109,6 +109,18 @@ <h3 class="text-sm text-gray-600 mb-2">{% trans "Click-Through Rate" %}</h3>
<canvas id="clickRateChart"></canvas>
</div>
</div>
<div class="p-4 bg-white rounded border">
<h3 class="text-sm text-gray-600 mb-2">{% trans "Subscribers" %}</h3>
<div style="height: 150px; position: relative;">
{% if campaign.subscriber_list %}
<canvas id="subscriberTrendChart"></canvas>
{% else %}
<div class="h-full flex items-center justify-center text-center px-3">
<p class="text-sm text-gray-500 font-medium">{% trans "No subscribers found for this campaign" %}</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>

Expand Down Expand Up @@ -200,6 +212,7 @@ <h2 class="text-lg font-semibold text-gray-900 mb-2">{% 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() {
Expand Down Expand Up @@ -253,11 +266,12 @@ <h2 class="text-lg font-semibold text-gray-900 mb-2">{% 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: {
Expand Down Expand Up @@ -297,6 +311,45 @@ <h2 class="text-lg font-semibold text-gray-900 mb-2">{% 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');
Expand All @@ -320,7 +373,7 @@ <h2 class="text-lg font-semibold text-gray-900 mb-2">{% trans "Per-email breakdo
tension: 0.4
}]
},
options: commonOptions
options: rateChartOptions
});
console.log('Delivery chart created:', deliveryChart);
} else {
Expand Down Expand Up @@ -349,7 +402,7 @@ <h2 class="text-lg font-semibold text-gray-900 mb-2">{% trans "Per-email breakdo
tension: 0.4
}]
},
options: commonOptions
options: rateChartOptions
});
console.log('Open chart created:', openChart);
} else {
Expand Down Expand Up @@ -378,12 +431,39 @@ <h2 class="text-lg font-semibold text-gray-900 mb-2">{% 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);
Expand Down