4.1.3
This commit is contained in:
247
controllers/panel/analytics/analyticsPanelController.js
Normal file
247
controllers/panel/analytics/analyticsPanelController.js
Normal file
@@ -0,0 +1,247 @@
|
||||
const UserModel = require('../../../models/UserModel')
|
||||
const AnalyticsEvent = require('../../../models/AnalyticsEventModel')
|
||||
|
||||
function parseDays(req) {
|
||||
const days = parseInt(req.query.days, 10)
|
||||
if (!days || days <= 0) return 30
|
||||
return Math.min(days, 365)
|
||||
}
|
||||
|
||||
// آمار جغرافیایی/جنسیت مستقیم از خودِ پروفایل کاربرا میاد (چیزی که موقع ثبتنام/ویرایش
|
||||
// پروفایل قبلاً ذخیره میشه) — نیازی به ردیابی جداگانه نداره
|
||||
async function getDemographics(req, res, next) {
|
||||
try {
|
||||
const [countries, provinces, cities, genders] = await Promise.all([
|
||||
UserModel.aggregate([
|
||||
{ $match: { 'country.name': { $exists: true, $ne: null } } },
|
||||
{ $group: { _id: '$country.name', count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: 20 }
|
||||
]),
|
||||
UserModel.aggregate([
|
||||
{ $match: { 'province.name': { $exists: true, $ne: null } } },
|
||||
{ $group: { _id: '$province.name', count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: 20 }
|
||||
]),
|
||||
UserModel.aggregate([
|
||||
{ $match: { 'city.name': { $exists: true, $ne: null } } },
|
||||
{ $group: { _id: '$city.name', count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: 20 }
|
||||
]),
|
||||
UserModel.aggregate([
|
||||
{ $group: { _id: '$gender', count: { $sum: 1 } } }
|
||||
])
|
||||
])
|
||||
|
||||
const genderMap = { male: 0, female: 0, unspecified: 0 }
|
||||
genders.forEach((g) => {
|
||||
if (g._id === 'male') genderMap.male = g.count
|
||||
else if (g._id === 'female') genderMap.female = g.count
|
||||
else genderMap.unspecified += g.count
|
||||
})
|
||||
|
||||
res.status(200).json({
|
||||
countries: countries.map((c) => ({ name: c._id, count: c.count })),
|
||||
provinces: provinces.map((p) => ({ name: p._id, count: p.count })),
|
||||
cities: cities.map((c) => ({ name: c._id, count: c.count })),
|
||||
gender: genderMap
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
// دستگاه/مرورگر/سیستمعامل/پلتفرم — از رویدادهای خام AnalyticsEvent (فقط بازهی
|
||||
// اخیر، برای پرفورمنس)
|
||||
async function getDeviceStats(req, res, next) {
|
||||
try {
|
||||
const since = new Date(Date.now() - parseDays(req) * 24 * 60 * 60 * 1000)
|
||||
const match = { createdAt: { $gte: since } }
|
||||
|
||||
const [devices, browsers, os, platforms] = await Promise.all([
|
||||
AnalyticsEvent.aggregate([
|
||||
{ $match: { ...match, device: { $ne: null } } },
|
||||
{ $group: { _id: '$device', count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: 15 }
|
||||
]),
|
||||
AnalyticsEvent.aggregate([
|
||||
{ $match: { ...match, browser: { $ne: null } } },
|
||||
{ $group: { _id: '$browser', count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: 15 }
|
||||
]),
|
||||
AnalyticsEvent.aggregate([
|
||||
{ $match: { ...match, os: { $ne: null } } },
|
||||
{ $group: { _id: '$os', count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: 15 }
|
||||
]),
|
||||
AnalyticsEvent.aggregate([
|
||||
{ $match: { ...match, type: 'session_start' } },
|
||||
{ $group: { _id: '$platform', count: { $sum: 1 } } }
|
||||
])
|
||||
])
|
||||
|
||||
res.status(200).json({
|
||||
devices: devices.map((d) => ({ name: d._id, count: d.count })),
|
||||
browsers: browsers.map((b) => ({ name: b._id, count: b.count })),
|
||||
os: os.map((o) => ({ name: o._id, count: o.count })),
|
||||
platforms: platforms.reduce(
|
||||
(acc, p) => ({ ...acc, [p._id]: p.count }),
|
||||
{ web: 0, android: 0, ios: 0 }
|
||||
)
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
// پربازدیدترین صفحات + میانگین زمان موندگاری روی هرکدوم
|
||||
async function getPageStats(req, res, next) {
|
||||
try {
|
||||
const since = new Date(Date.now() - parseDays(req) * 24 * 60 * 60 * 1000)
|
||||
|
||||
const pages = await AnalyticsEvent.aggregate([
|
||||
{ $match: { type: 'page_view', createdAt: { $gte: since }, path: { $ne: null } } },
|
||||
{
|
||||
$group: {
|
||||
_id: '$path',
|
||||
views: { $sum: 1 },
|
||||
avgDurationMs: { $avg: '$durationMs' }
|
||||
}
|
||||
},
|
||||
{ $sort: { views: -1 } },
|
||||
{ $limit: 30 }
|
||||
])
|
||||
|
||||
res.status(200).json({
|
||||
pages: pages.map((p) => ({
|
||||
path: p._id,
|
||||
views: p.views,
|
||||
avgDurationMs: Math.round(p.avgDurationMs || 0)
|
||||
}))
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
// پرکلیکترین عناصر (label ارسالشده از کلاینت — data-analytics-id یا متن دکمه)
|
||||
async function getClickStats(req, res, next) {
|
||||
try {
|
||||
const since = new Date(Date.now() - parseDays(req) * 24 * 60 * 60 * 1000)
|
||||
|
||||
const clicks = await AnalyticsEvent.aggregate([
|
||||
{ $match: { type: 'click', createdAt: { $gte: since }, label: { $ne: null } } },
|
||||
{ $group: { _id: { label: '$label', path: '$path' }, count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: 30 }
|
||||
])
|
||||
|
||||
res.status(200).json({
|
||||
clicks: clicks.map((c) => ({
|
||||
label: c._id.label,
|
||||
path: c._id.path,
|
||||
count: c.count
|
||||
}))
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
// نرخ بازگشت — بر اساس last_online کاربر نسبت به createdAt؛ کاربری که last_onlineش
|
||||
// خیلی نزدیک به createdAtشه (یا اصلاً last_online نداره) یعنی بعد از ثبتنام دیگه
|
||||
// برنگشته
|
||||
async function getRetentionStats(req, res, next) {
|
||||
try {
|
||||
const now = Date.now()
|
||||
const day7 = new Date(now - 7 * 24 * 60 * 60 * 1000)
|
||||
const day30 = new Date(now - 30 * 24 * 60 * 60 * 1000)
|
||||
|
||||
const [totalUsers, activeLast7Days, activeLast30Days, neverReturned] =
|
||||
await Promise.all([
|
||||
UserModel.countDocuments({}),
|
||||
UserModel.countDocuments({ last_online: { $gte: day7 } }),
|
||||
UserModel.countDocuments({ last_online: { $gte: day30 } }),
|
||||
UserModel.countDocuments({
|
||||
$or: [
|
||||
{ last_online: null },
|
||||
{ $expr: { $lte: [{ $subtract: ['$last_online', '$createdAt'] }, 60000] } }
|
||||
]
|
||||
})
|
||||
])
|
||||
|
||||
res.status(200).json({
|
||||
totalUsers,
|
||||
activeLast7Days,
|
||||
activeLast30Days,
|
||||
neverReturned,
|
||||
returningRate:
|
||||
totalUsers > 0
|
||||
? Math.round(((totalUsers - neverReturned) / totalUsers) * 100)
|
||||
: 0
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
// خلاصهی کلی نشستها — تعداد نشست، میانگین صفحاتِ هر نشست، میانگین زمانِ هر نشست
|
||||
// (مجموع durationMsِ page_viewهای همون sessionId)
|
||||
async function getSessionOverview(req, res, next) {
|
||||
try {
|
||||
const since = new Date(Date.now() - parseDays(req) * 24 * 60 * 60 * 1000)
|
||||
|
||||
const [sessionCountAgg, sessionDurationAgg] = await Promise.all([
|
||||
AnalyticsEvent.aggregate([
|
||||
{ $match: { type: 'session_start', createdAt: { $gte: since } } },
|
||||
{ $group: { _id: null, count: { $sum: 1 } } }
|
||||
]),
|
||||
AnalyticsEvent.aggregate([
|
||||
{
|
||||
$match: {
|
||||
type: 'page_view',
|
||||
createdAt: { $gte: since },
|
||||
durationMs: { $ne: null }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$sessionId',
|
||||
pages: { $sum: 1 },
|
||||
totalDurationMs: { $sum: '$durationMs' }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgPagesPerSession: { $avg: '$pages' },
|
||||
avgSessionDurationMs: { $avg: '$totalDurationMs' },
|
||||
sessionsWithDuration: { $sum: 1 }
|
||||
}
|
||||
}
|
||||
])
|
||||
])
|
||||
|
||||
res.status(200).json({
|
||||
totalSessions: sessionCountAgg[0]?.count || 0,
|
||||
avgPagesPerSession: Math.round(sessionDurationAgg[0]?.avgPagesPerSession || 0),
|
||||
avgSessionDurationMs: Math.round(sessionDurationAgg[0]?.avgSessionDurationMs || 0)
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getDemographics,
|
||||
getDeviceStats,
|
||||
getPageStats,
|
||||
getClickStats,
|
||||
getRetentionStats,
|
||||
getSessionOverview
|
||||
}
|
||||
Reference in New Issue
Block a user