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();