tractatus/src/controllers/analytics.controller.js
TheFlow ac2db33732 fix(submissions): restructure Economist package and fix article display
- Create Economist SubmissionTracking package correctly:
  * mainArticle = full blog post content
  * coverLetter = 216-word SIR— letter
  * Links to blog post via blogPostId
- Archive 'Letter to The Economist' from blog posts (it's the cover letter)
- Fix date display on article cards (use published_at)
- Target publication already displaying via blue badge

Database changes:
- Make blogPostId optional in SubmissionTracking model
- Economist package ID: 68fa85ae49d4900e7f2ecd83
- Le Monde package ID: 68fa2abd2e6acd5691932150

Next: Enhanced modal with tabs, validation, export

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 08:47:42 +13:00

296 lines
6.9 KiB
JavaScript

/**
* Analytics Controller
* API endpoints for analytics dashboard
*/
const { PageView, Session } = require('../models/Analytics.model');
/**
* Get analytics overview
*/
async function getOverview(req, res) {
try {
const { period = '24h' } = req.query;
// Calculate date range
let startDate;
const endDate = new Date();
switch (period) {
case '1h':
startDate = new Date(Date.now() - 60 * 60 * 1000);
break;
case '24h':
startDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
break;
case '7d':
startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
break;
case '30d':
startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
break;
default:
startDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
}
// Get real-time visitors
const realTimeVisitors = await Session.getRealTimeVisitors();
// Get total page views
const totalPageViews = await PageView.getPageViews(startDate, endDate);
// Get unique visitors
const uniqueVisitors = await PageView.getUniqueVisitors(startDate, endDate);
// Get bounce rate
const bounceRate = await Session.getBounceRate(startDate, endDate);
// Get average session duration
const avgDuration = await Session.getAverageDuration(startDate, endDate);
// Get top pages
const topPages = await PageView.getTopPages(startDate, endDate, 10);
// Get top referrers
const topReferrers = await PageView.getTopReferrers(startDate, endDate, 10);
res.json({
success: true,
period,
data: {
realTimeVisitors,
totalPageViews,
uniqueVisitors,
bounceRate: Math.round(bounceRate * 10) / 10,
avgDuration,
topPages,
topReferrers
}
});
} catch (error) {
console.error('[Analytics] Overview error:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch analytics overview'
});
}
}
/**
* Get hourly trend data
*/
async function getHourlyTrend(req, res) {
try {
const { hours = 24 } = req.query;
const startDate = new Date(Date.now() - parseInt(hours, 10) * 60 * 60 * 1000);
const endDate = new Date();
const hourlyData = await PageView.getHourlyViews(startDate, endDate);
res.json({
success: true,
data: hourlyData
});
} catch (error) {
console.error('[Analytics] Hourly trend error:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch hourly trend'
});
}
}
/**
* Get live visitors (for real-time dashboard)
*/
async function getLiveVisitors(req, res) {
try {
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
const activeSessions = await Session.find({
isActive: true,
endTime: { $gte: fiveMinutesAgo }
}).select('entryPage exitPage pages startTime endTime language');
const currentPages = await PageView.find({
timestamp: { $gte: fiveMinutesAgo }
})
.sort({ timestamp: -1 })
.limit(20)
.select('path timestamp visitorId sessionId');
res.json({
success: true,
data: {
count: activeSessions.length,
sessions: activeSessions,
recentPages: currentPages
}
});
} catch (error) {
console.error('[Analytics] Live visitors error:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch live visitors'
});
}
}
/**
* Get page-specific analytics
*/
async function getPageAnalytics(req, res) {
try {
const { path, period = '7d' } = req.query;
if (!path) {
return res.status(400).json({
success: false,
error: 'Page path required'
});
}
// Calculate date range
let startDate;
const endDate = new Date();
switch (period) {
case '24h':
startDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
break;
case '7d':
startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
break;
case '30d':
startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
break;
default:
startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
}
const pageViews = await PageView.countDocuments({
path,
timestamp: { $gte: startDate, $lte: endDate }
});
const uniqueVisitors = await PageView.distinct('visitorId', {
path,
timestamp: { $gte: startDate, $lte: endDate }
});
const avgDuration = await PageView.aggregate([
{
$match: {
path,
timestamp: { $gte: startDate, $lte: endDate },
duration: { $exists: true, $gt: 0 }
}
},
{
$group: {
_id: null,
avgDuration: { $avg: '$duration' }
}
}
]);
res.json({
success: true,
path,
period,
data: {
pageViews,
uniqueVisitors: uniqueVisitors.length,
avgDuration: avgDuration.length > 0 ? Math.round(avgDuration[0].avgDuration) : 0
}
});
} catch (error) {
console.error('[Analytics] Page analytics error:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch page analytics'
});
}
}
/**
* Get UTM campaign performance
*/
async function getCampaignAnalytics(req, res) {
try {
const { period = '30d' } = req.query;
let startDate;
const endDate = new Date();
switch (period) {
case '7d':
startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
break;
case '30d':
startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
break;
case '90d':
startDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
break;
default:
startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
}
const campaigns = await PageView.aggregate([
{
$match: {
timestamp: { $gte: startDate, $lte: endDate },
utmCampaign: { $exists: true, $ne: null }
}
},
{
$group: {
_id: {
campaign: '$utmCampaign',
source: '$utmSource',
medium: '$utmMedium'
},
visits: { $sum: 1 },
uniqueVisitors: { $addToSet: '$visitorId' }
}
},
{
$project: {
campaign: '$_id.campaign',
source: '$_id.source',
medium: '$_id.medium',
visits: 1,
uniqueVisitors: { $size: '$uniqueVisitors' }
}
},
{
$sort: { visits: -1 }
},
{
$limit: 20
}
]);
res.json({
success: true,
period,
data: campaigns
});
} catch (error) {
console.error('[Analytics] Campaign analytics error:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch campaign analytics'
});
}
}
module.exports = {
getOverview,
getHourlyTrend,
getLiveVisitors,
getPageAnalytics,
getCampaignAnalytics
};