- Implemented a comprehensive health check utility to monitor system dependencies including NocoDB, SMTP, Represent API, disk space, and memory usage. - Created a logger utility using Winston for structured logging with daily rotation and various log levels. - Developed a metrics utility using Prometheus client to track application performance metrics such as email sends, HTTP requests, and user activity. - Added a backup script for automated backups of NocoDB data, uploaded files, and environment configurations with optional S3 support. - Introduced a toggle script to switch between development (MailHog) and production (ProtonMail) SMTP configurations.
351 lines
9.8 KiB
JavaScript
351 lines
9.8 KiB
JavaScript
const logger = require('./logger');
|
|
|
|
/**
|
|
* Analytics tracking utilities for the Influence application
|
|
* Provides helpers for tracking campaign conversion, representative response rates,
|
|
* user retention, and geographic participation
|
|
*/
|
|
class Analytics {
|
|
constructor() {
|
|
this.cache = {
|
|
campaignStats: new Map(),
|
|
userRetention: new Map(),
|
|
geographicData: new Map()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Track campaign conversion rate
|
|
* @param {string} campaignId - Campaign identifier
|
|
* @param {number} visitors - Number of unique visitors
|
|
* @param {number} participants - Number of participants who took action
|
|
*/
|
|
trackCampaignConversion(campaignId, visitors, participants) {
|
|
const conversionRate = visitors > 0 ? (participants / visitors) * 100 : 0;
|
|
|
|
const stats = {
|
|
campaignId,
|
|
visitors,
|
|
participants,
|
|
conversionRate: conversionRate.toFixed(2),
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
this.cache.campaignStats.set(campaignId, stats);
|
|
|
|
logger.info('Campaign conversion tracked', {
|
|
event: 'analytics_campaign_conversion',
|
|
...stats
|
|
});
|
|
|
|
return stats;
|
|
}
|
|
|
|
/**
|
|
* Calculate representative response rate
|
|
* @param {string} representativeLevel - Level (Federal, Provincial, Municipal)
|
|
* @param {number} emailsSent - Total emails sent
|
|
* @param {number} responsesReceived - Total responses received
|
|
*/
|
|
calculateRepresentativeResponseRate(representativeLevel, emailsSent, responsesReceived) {
|
|
const responseRate = emailsSent > 0 ? (responsesReceived / emailsSent) * 100 : 0;
|
|
|
|
const stats = {
|
|
representativeLevel,
|
|
emailsSent,
|
|
responsesReceived,
|
|
responseRate: responseRate.toFixed(2),
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
logger.info('Representative response rate calculated', {
|
|
event: 'analytics_response_rate',
|
|
...stats
|
|
});
|
|
|
|
return stats;
|
|
}
|
|
|
|
/**
|
|
* Track user retention
|
|
* @param {string} userId - User identifier
|
|
* @param {string} action - Action type (login, campaign_participation, etc.)
|
|
*/
|
|
trackUserRetention(userId, action) {
|
|
let userData = this.cache.userRetention.get(userId) || {
|
|
userId,
|
|
firstSeen: new Date().toISOString(),
|
|
lastSeen: new Date().toISOString(),
|
|
actionCount: 0,
|
|
actions: []
|
|
};
|
|
|
|
userData.lastSeen = new Date().toISOString();
|
|
userData.actionCount += 1;
|
|
userData.actions.push({
|
|
action,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
// Keep only last 100 actions per user
|
|
if (userData.actions.length > 100) {
|
|
userData.actions = userData.actions.slice(-100);
|
|
}
|
|
|
|
this.cache.userRetention.set(userId, userData);
|
|
|
|
// Determine if user is one-time or repeat
|
|
const daysBetween = this.getDaysBetween(userData.firstSeen, userData.lastSeen);
|
|
const isRepeatUser = daysBetween > 0 || userData.actionCount > 1;
|
|
|
|
logger.info('User retention tracked', {
|
|
event: 'analytics_user_retention',
|
|
userId,
|
|
action,
|
|
isRepeatUser,
|
|
totalActions: userData.actionCount
|
|
});
|
|
|
|
return { ...userData, isRepeatUser };
|
|
}
|
|
|
|
/**
|
|
* Track geographic participation by postal code
|
|
* @param {string} postalCode - Canadian postal code
|
|
* @param {string} campaignId - Campaign identifier
|
|
*/
|
|
trackGeographicParticipation(postalCode, campaignId) {
|
|
const postalPrefix = postalCode.substring(0, 3).toUpperCase();
|
|
|
|
let geoData = this.cache.geographicData.get(postalPrefix) || {
|
|
postalPrefix,
|
|
participationCount: 0,
|
|
campaigns: new Set()
|
|
};
|
|
|
|
geoData.participationCount += 1;
|
|
geoData.campaigns.add(campaignId);
|
|
|
|
this.cache.geographicData.set(postalPrefix, geoData);
|
|
|
|
logger.info('Geographic participation tracked', {
|
|
event: 'analytics_geographic_participation',
|
|
postalPrefix,
|
|
campaignId,
|
|
totalParticipation: geoData.participationCount
|
|
});
|
|
|
|
return {
|
|
postalPrefix,
|
|
participationCount: geoData.participationCount,
|
|
uniqueCampaigns: geoData.campaigns.size
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get geographic heatmap data
|
|
* @returns {Array} Array of postal code prefixes with participation counts
|
|
*/
|
|
getGeographicHeatmap() {
|
|
const heatmapData = [];
|
|
|
|
for (const [postalPrefix, data] of this.cache.geographicData.entries()) {
|
|
heatmapData.push({
|
|
postalPrefix,
|
|
participationCount: data.participationCount,
|
|
uniqueCampaigns: data.campaigns.size
|
|
});
|
|
}
|
|
|
|
return heatmapData.sort((a, b) => b.participationCount - a.participationCount);
|
|
}
|
|
|
|
/**
|
|
* Analyze peak usage times
|
|
* @param {Array} events - Array of event objects with timestamps
|
|
* @returns {Object} Peak usage analysis
|
|
*/
|
|
analyzePeakUsageTimes(events) {
|
|
const hourCounts = new Array(24).fill(0);
|
|
const dayOfWeekCounts = new Array(7).fill(0);
|
|
|
|
events.forEach(event => {
|
|
const date = new Date(event.timestamp);
|
|
const hour = date.getHours();
|
|
const dayOfWeek = date.getDay();
|
|
|
|
hourCounts[hour] += 1;
|
|
dayOfWeekCounts[dayOfWeek] += 1;
|
|
});
|
|
|
|
const peakHour = hourCounts.indexOf(Math.max(...hourCounts));
|
|
const peakDay = dayOfWeekCounts.indexOf(Math.max(...dayOfWeekCounts));
|
|
|
|
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
|
|
const analysis = {
|
|
peakHour: `${peakHour}:00`,
|
|
peakDay: dayNames[peakDay],
|
|
hourlyDistribution: hourCounts,
|
|
dailyDistribution: dayOfWeekCounts,
|
|
totalEvents: events.length
|
|
};
|
|
|
|
logger.info('Peak usage times analyzed', {
|
|
event: 'analytics_peak_usage',
|
|
...analysis
|
|
});
|
|
|
|
return analysis;
|
|
}
|
|
|
|
/**
|
|
* Track device breakdown
|
|
* @param {string} userAgent - User agent string
|
|
*/
|
|
trackDeviceType(userAgent) {
|
|
const isMobile = /Mobile|Android|iPhone|iPad|iPod/i.test(userAgent);
|
|
const isTablet = /iPad|Android.*Tablet/i.test(userAgent);
|
|
const isDesktop = !isMobile && !isTablet;
|
|
|
|
const deviceType = isTablet ? 'tablet' : isMobile ? 'mobile' : 'desktop';
|
|
|
|
logger.info('Device type tracked', {
|
|
event: 'analytics_device_type',
|
|
deviceType,
|
|
userAgent: userAgent.substring(0, 100)
|
|
});
|
|
|
|
return deviceType;
|
|
}
|
|
|
|
/**
|
|
* Track email deliverability
|
|
* @param {string} status - Status (success, bounce, spam, failed)
|
|
* @param {string} campaignId - Campaign identifier
|
|
* @param {Object} details - Additional details
|
|
*/
|
|
trackEmailDeliverability(status, campaignId, details = {}) {
|
|
const deliverabilityData = {
|
|
status,
|
|
campaignId,
|
|
timestamp: new Date().toISOString(),
|
|
...details
|
|
};
|
|
|
|
logger.info('Email deliverability tracked', {
|
|
event: 'analytics_email_deliverability',
|
|
...deliverabilityData
|
|
});
|
|
|
|
return deliverabilityData;
|
|
}
|
|
|
|
/**
|
|
* Generate campaign analytics report
|
|
* @param {string} campaignId - Campaign identifier
|
|
* @param {Object} data - Campaign data including visitors, participants, emails sent, etc.
|
|
*/
|
|
generateCampaignReport(campaignId, data) {
|
|
const {
|
|
visitors = 0,
|
|
participants = 0,
|
|
emailsSent = 0,
|
|
emailsFailed = 0,
|
|
responsesReceived = 0,
|
|
shareCount = 0,
|
|
avgTimeOnPage = 0
|
|
} = data;
|
|
|
|
const conversionRate = visitors > 0 ? ((participants / visitors) * 100).toFixed(2) : 0;
|
|
const emailSuccessRate = emailsSent > 0 ? (((emailsSent - emailsFailed) / emailsSent) * 100).toFixed(2) : 0;
|
|
const responseRate = emailsSent > 0 ? ((responsesReceived / emailsSent) * 100).toFixed(2) : 0;
|
|
const shareRate = participants > 0 ? ((shareCount / participants) * 100).toFixed(2) : 0;
|
|
|
|
const report = {
|
|
campaignId,
|
|
metrics: {
|
|
visitors,
|
|
participants,
|
|
conversionRate: `${conversionRate}%`,
|
|
emailsSent,
|
|
emailsFailed,
|
|
emailSuccessRate: `${emailSuccessRate}%`,
|
|
responsesReceived,
|
|
responseRate: `${responseRate}%`,
|
|
shareCount,
|
|
shareRate: `${shareRate}%`,
|
|
avgTimeOnPage: `${avgTimeOnPage}s`
|
|
},
|
|
insights: {
|
|
performance: conversionRate > 5 ? 'excellent' : conversionRate > 2 ? 'good' : 'needs improvement',
|
|
emailHealth: emailSuccessRate > 95 ? 'excellent' : emailSuccessRate > 85 ? 'good' : 'poor',
|
|
engagement: responseRate > 10 ? 'high' : responseRate > 3 ? 'medium' : 'low'
|
|
},
|
|
generatedAt: new Date().toISOString()
|
|
};
|
|
|
|
logger.info('Campaign report generated', {
|
|
event: 'analytics_campaign_report',
|
|
campaignId,
|
|
...report.metrics
|
|
});
|
|
|
|
return report;
|
|
}
|
|
|
|
/**
|
|
* Get user retention summary
|
|
* @returns {Object} User retention statistics
|
|
*/
|
|
getUserRetentionSummary() {
|
|
let oneTimeUsers = 0;
|
|
let repeatUsers = 0;
|
|
let totalActions = 0;
|
|
|
|
for (const [userId, userData] of this.cache.userRetention.entries()) {
|
|
totalActions += userData.actionCount;
|
|
|
|
const daysBetween = this.getDaysBetween(userData.firstSeen, userData.lastSeen);
|
|
if (daysBetween > 0 || userData.actionCount > 1) {
|
|
repeatUsers += 1;
|
|
} else {
|
|
oneTimeUsers += 1;
|
|
}
|
|
}
|
|
|
|
const totalUsers = oneTimeUsers + repeatUsers;
|
|
const retentionRate = totalUsers > 0 ? ((repeatUsers / totalUsers) * 100).toFixed(2) : 0;
|
|
|
|
return {
|
|
totalUsers,
|
|
oneTimeUsers,
|
|
repeatUsers,
|
|
retentionRate: `${retentionRate}%`,
|
|
avgActionsPerUser: totalUsers > 0 ? (totalActions / totalUsers).toFixed(2) : 0
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Helper: Calculate days between two dates
|
|
*/
|
|
getDaysBetween(date1, date2) {
|
|
const d1 = new Date(date1);
|
|
const d2 = new Date(date2);
|
|
const diffTime = Math.abs(d2 - d1);
|
|
return Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
|
}
|
|
|
|
/**
|
|
* Clear cache (for testing or memory management)
|
|
*/
|
|
clearCache() {
|
|
this.cache.campaignStats.clear();
|
|
this.cache.userRetention.clear();
|
|
this.cache.geographicData.clear();
|
|
logger.info('Analytics cache cleared');
|
|
}
|
|
}
|
|
|
|
module.exports = new Analytics();
|