diff --git a/package-lock.json b/package-lock.json index b3ed008..d0ed281 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,11 +15,12 @@ "@types/uuid": "^10.0.0", "bcrypt": "^6.0.0", "cors": "^2.8.5", + "date-fns-tz": "^3.2.0", "express": "^4.18.2", "ioredis": "^5.6.1", "jsonwebtoken": "^9.0.2", "nodemailer": "^7.0.9", - "uuid": "^13.0.0", + "uuid": "^8.3.2", "ws": "^8.18.2" }, "devDependencies": { @@ -2378,6 +2379,24 @@ "dev": true, "license": "MIT" }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -3851,16 +3870,11 @@ } }, "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "bin": { - "uuid": "dist-node/bin/uuid" + "uuid": "dist/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { diff --git a/package.json b/package.json index 151a517..960741a 100644 --- a/package.json +++ b/package.json @@ -33,11 +33,12 @@ "@types/uuid": "^10.0.0", "bcrypt": "^6.0.0", "cors": "^2.8.5", + "date-fns-tz": "^3.2.0", "express": "^4.18.2", "ioredis": "^5.6.1", "jsonwebtoken": "^9.0.2", "nodemailer": "^7.0.9", - "uuid": "^13.0.0", + "uuid": "^8.3.2", "ws": "^8.18.2" } } diff --git a/src/controllers/analyticsController.ts b/src/controllers/analyticsController.ts index 9893eb5..87f5361 100644 --- a/src/controllers/analyticsController.ts +++ b/src/controllers/analyticsController.ts @@ -4,20 +4,40 @@ import { getCurrentTabSession, incrementTabAggregate, getTabAggregates, clearTab import { flushPresenceForUser } from '../utils/flushPresence'; import { flushTabUsageForUser } from "../utils/flushTabUsage"; import { wsClients } from '../websocket/handler'; +import { toZonedTime, fromZonedTime } from "date-fns-tz"; //For Fixing the User and UTC time analytics Issue + +//Makes sure No garbage Value is sent by the user in else Time is set in UTC +function resolveTimezone(tz: unknown): string { + const zone = + Array.isArray(tz) ? tz[0] : + typeof tz === "string" ? tz : + null; + if (!zone) return "UTC"; + try { + Intl.DateTimeFormat(undefined, { timeZone: zone }); + return zone; + } catch { + return "UTC"; + } +} -// Helper to get start/end of a day -function getDayRange(date: Date) { - const start = new Date(date); - start.setHours(0, 0, 0, 0); - const end = new Date(date); - end.setHours(23, 59, 59, 999); - return { start, end }; +// Helper to get start/end of a day(according to the user time) +function getUserDayRange(date: Date, timezone: string) { + const zonedDate = toZonedTime(date, timezone); + const startZoned = new Date(zonedDate); + startZoned.setHours(0, 0, 0, 0); + const endZoned = new Date(zonedDate); + endZoned.setHours(23, 59, 59, 999); + const startUtc = fromZonedTime(startZoned, timezone); + const endUtc = fromZonedTime(endZoned, timezone); + return { start: startUtc, end: endUtc }; } // GET /api/analytics/presence/today export const getTodayPresence = async (req: Request, res: Response) => { const userId = req.user.id; - const { start, end } = getDayRange(new Date()); + const timezone = resolveTimezone(req.query.timezone); + const { start, end } = getUserDayRange(new Date(), timezone); const sessions = await prisma.presenceSession.findMany({ where: { userId, @@ -31,12 +51,13 @@ export const getTodayPresence = async (req: Request, res: Response) => { // GET /api/analytics/presence/weekly export const getWeeklyPresence = async (req: Request, res: Response) => { + const timezone = resolveTimezone(req.query.timezone); const userId = req.user.id; const days: { date: string, totalSeconds: number }[] = []; for (let i = 6; i >= 0; i--) { const date = new Date(); date.setDate(date.getDate() - i); - const { start, end } = getDayRange(date); + const { start, end } = getUserDayRange(date, timezone); const sessions = await prisma.presenceSession.findMany({ where: { userId, @@ -45,15 +66,16 @@ export const getWeeklyPresence = async (req: Request, res: Response) => { }); const totalSeconds = sessions.reduce((sum, s) => sum + (s.duration ?? (s.endTime ? Math.floor((s.endTime.getTime() - s.startTime.getTime()) / 1000) : 0)), 0); - days.push({ date: start.toISOString().slice(0, 10), totalSeconds }); + days.push({ date: toZonedTime(start, timezone).toISOString().slice(0, 10), totalSeconds }); } res.json({ success: true, days }); }; // GET /api/analytics/tab-usage/today export const getTodayTabUsage = async (req: Request, res: Response) => { + const timezone = resolveTimezone(req.query.timezone); const userId = req.user.id; - const { start, end } = getDayRange(new Date()); + const { start, end } = getUserDayRange(new Date(), timezone); const usages = await prisma.tabUsage.findMany({ where: { userId, @@ -66,12 +88,14 @@ export const getTodayTabUsage = async (req: Request, res: Response) => { // GET /api/analytics/tab-usage/weekly export const getWeeklyTabUsage = async (req: Request, res: Response) => { + const timezone = resolveTimezone(req.query.timezone); const userId = req.user.id; const weekData: { date: string, domains: { domain: string, seconds: number }[] }[] = []; + const userNow = toZonedTime(new Date(), timezone); for (let i = 6; i >= 0; i--) { - const date = new Date(); - date.setDate(date.getDate() - i); - const { start, end } = getDayRange(date); + const userDate = new Date(userNow); + userDate.setDate(userNow.getDate() - i); + const { start, end } = getUserDayRange(userDate, timezone); const usages = await prisma.tabUsage.findMany({ where: { userId, @@ -79,7 +103,7 @@ export const getWeeklyTabUsage = async (req: Request, res: Response) => { } }); weekData.push({ - date: start.toISOString().slice(0, 10), + date: userDate.toISOString().slice(0, 10), domains: usages.map(u => ({ domain: u.domain, seconds: u.seconds })) }); } @@ -89,6 +113,7 @@ export const getWeeklyTabUsage = async (req: Request, res: Response) => { // GET /api/analytics/presence/hourly?days=7 export const getHourlyPresence = async (req: Request, res: Response) => { + const timezone = resolveTimezone(req.query.timezone); const userId = req.user.id; const days = Number(req.query.days) || 7; const now = new Date(); @@ -101,10 +126,10 @@ export const getHourlyPresence = async (req: Request, res: Response) => { for (let i = days - 1; i >= 0; i--) { const date = new Date(now); date.setDate(now.getDate() - i); - const { start, end } = getDayRange(date); + const { start, end } = getUserDayRange(date, timezone); - if (!startDate) startDate = start.toISOString().slice(0, 10); - endDate = end.toISOString().slice(0, 10); + if (!startDate) startDate = toZonedTime(start, timezone).toISOString().slice(0, 10); + endDate = toZonedTime(end, timezone).toISOString().slice(0, 10); const sessions = await prisma.presenceSession.findMany({ where: { @@ -121,10 +146,12 @@ export const getHourlyPresence = async (req: Request, res: Response) => { if (s < start) s = start; while (s < e) { - const hour = s.getHours(); - const nextHour = new Date(s); - nextHour.setHours(hour + 1, 0, 0, 0); - const segmentEnd = nextHour < e ? nextHour : e; + const zonedS = toZonedTime(s, timezone); + const hour = zonedS.getHours(); + const nextHourZoned = new Date(zonedS); + nextHourZoned.setHours(hour + 1, 0, 0, 0); + const nextHourUtc = fromZonedTime(nextHourZoned, timezone); + const segmentEnd = nextHourUtc < e ? nextHourUtc : e; const seconds = Math.floor((segmentEnd.getTime() - s.getTime()) / 1000); hours[hour] += seconds; totalSeconds += seconds;