This commit is contained in:
payacom
2026-09-13 21:32:36 +03:30
parent 90c5f94b57
commit c302aea177
49 changed files with 4063 additions and 920 deletions

View 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
}