/** * Analytics Model - Sovereign Analytics Tracking * Privacy-respecting, self-hosted analytics (no cookies, no external services) */ const mongoose = require('mongoose'); const PageViewSchema = new mongoose.Schema({ // Page Information path: { type: String, required: true, index: true }, title: { type: String }, referrer: { type: String }, // Visitor Information (privacy-respecting) visitorId: { type: String, required: true, index: true }, // Hashed IP + User Agent sessionId: { type: String, required: true }, // Session identifier (indexed via compound index below) // Request Details userAgent: { type: String }, language: { type: String }, country: { type: String }, // Optional: derived from IP if available // Timing timestamp: { type: Date, default: Date.now, index: true }, duration: { type: Number }, // Time spent on page (if available) // Metadata isUnique: { type: Boolean, default: false }, // First visit of the day isBounce: { type: Boolean, default: false }, // Only page visited in session // UTM Parameters (marketing attribution) utmSource: { type: String }, utmMedium: { type: String }, utmCampaign: { type: String }, utmContent: { type: String }, utmTerm: { type: String } }, { timestamps: true }); // Indexes for performance PageViewSchema.index({ timestamp: -1 }); PageViewSchema.index({ path: 1, timestamp: -1 }); PageViewSchema.index({ visitorId: 1, timestamp: -1 }); PageViewSchema.index({ sessionId: 1, timestamp: -1 }); // Session Schema const SessionSchema = new mongoose.Schema({ sessionId: { type: String, required: true, unique: true }, // unique already creates index visitorId: { type: String, required: true, index: true }, startTime: { type: Date, default: Date.now }, endTime: { type: Date }, duration: { type: Number }, // Total session duration in seconds pageViews: { type: Number, default: 1 }, pages: [{ type: String }], // Paths visited entryPage: { type: String }, exitPage: { type: String }, referrer: { type: String }, utmSource: { type: String }, utmMedium: { type: String }, utmCampaign: { type: String }, country: { type: String }, language: { type: String }, isActive: { type: Boolean, default: true } // Session still active }, { timestamps: true }); SessionSchema.index({ sessionId: 1 }); SessionSchema.index({ visitorId: 1, startTime: -1 }); SessionSchema.index({ startTime: -1 }); // Statics for analytics queries /** * Get real-time visitor count (last 5 minutes) */ SessionSchema.statics.getRealTimeVisitors = async function() { const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); return await this.countDocuments({ isActive: true, endTime: { $gte: fiveMinutesAgo } }); }; /** * Get total page views for a date range */ PageViewSchema.statics.getPageViews = async function(startDate, endDate) { return await this.countDocuments({ timestamp: { $gte: startDate, $lte: endDate } }); }; /** * Get unique visitors for a date range */ PageViewSchema.statics.getUniqueVisitors = async function(startDate, endDate) { const result = await this.distinct('visitorId', { timestamp: { $gte: startDate, $lte: endDate } }); return result.length; }; /** * Get top pages by views */ PageViewSchema.statics.getTopPages = async function(startDate, endDate, limit = 10) { return await this.aggregate([ { $match: { timestamp: { $gte: startDate, $lte: endDate } } }, { $group: { _id: '$path', views: { $sum: 1 }, uniqueVisitors: { $addToSet: '$visitorId' } } }, { $project: { path: '$_id', views: 1, uniqueVisitors: { $size: '$uniqueVisitors' } } }, { $sort: { views: -1 } }, { $limit: limit } ]); }; /** * Get top referrers */ PageViewSchema.statics.getTopReferrers = async function(startDate, endDate, limit = 10) { return await this.aggregate([ { $match: { timestamp: { $gte: startDate, $lte: endDate }, referrer: { $exists: true, $ne: '' } } }, { $group: { _id: '$referrer', visits: { $sum: 1 } } }, { $project: { referrer: '$_id', visits: 1 } }, { $sort: { visits: -1 } }, { $limit: limit } ]); }; /** * Get hourly page views (for trend charts) */ PageViewSchema.statics.getHourlyViews = async function(startDate, endDate) { return await this.aggregate([ { $match: { timestamp: { $gte: startDate, $lte: endDate } } }, { $group: { _id: { year: { $year: '$timestamp' }, month: { $month: '$timestamp' }, day: { $dayOfMonth: '$timestamp' }, hour: { $hour: '$timestamp' } }, views: { $sum: 1 }, uniqueVisitors: { $addToSet: '$visitorId' } } }, { $project: { hour: '$_id', views: 1, uniqueVisitors: { $size: '$uniqueVisitors' } } }, { $sort: { '_id.year': 1, '_id.month': 1, '_id.day': 1, '_id.hour': 1 } } ]); }; /** * Get bounce rate */ SessionSchema.statics.getBounceRate = async function(startDate, endDate) { const totalSessions = await this.countDocuments({ startTime: { $gte: startDate, $lte: endDate } }); const bouncedSessions = await this.countDocuments({ startTime: { $gte: startDate, $lte: endDate }, pageViews: 1 }); return totalSessions > 0 ? (bouncedSessions / totalSessions) * 100 : 0; }; /** * Get average session duration */ SessionSchema.statics.getAverageDuration = async function(startDate, endDate) { const result = await this.aggregate([ { $match: { startTime: { $gte: startDate, $lte: endDate }, duration: { $exists: true, $gt: 0 } } }, { $group: { _id: null, avgDuration: { $avg: '$duration' } } } ]); return result.length > 0 ? Math.round(result[0].avgDuration) : 0; }; const PageView = mongoose.model('PageView', PageViewSchema); const Session = mongoose.model('Session', SessionSchema); module.exports = { PageView, Session };