Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Comment on lines 33 to +41
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uuid has been downgraded to ^8.3.2 while @types/uuid remains at ^10.0.0, so the runtime library and its type definitions are now on different major versions. Even though this may still work at runtime, keeping the versions aligned reduces the risk of subtle type/runtime mismatches and makes future upgrades easier; consider pinning @types/uuid to a version compatible with uuid@8 or documenting why this intentional mismatch is safe.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Suyash-ka-github, please revert the UUID version changes.

"ws": "^8.18.2"
}
}
71 changes: 49 additions & 22 deletions src/controllers/analyticsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -66,20 +88,22 @@ 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,
date: { gte: start, lte: end }
}
});
weekData.push({
date: start.toISOString().slice(0, 10),
date: userDate.toISOString().slice(0, 10),
Comment on lines 105 to +106
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The date field in the weekly tab-usage response is derived from userDate.toISOString().slice(0, 10), but userDate is not the same value used for the aggregation window and has already gone through timezone conversion. This can produce a day label that doesn’t align with the actual [start, end] range used for the query, especially when timezone offsets or DST transitions are involved. To keep the label consistent with the data (and with getWeeklyPresence), derive the date string from the computed start boundary converted to the user’s timezone instead of from userDate.

Copilot uses AI. Check for mistakes.
domains: usages.map(u => ({ domain: u.domain, seconds: u.seconds }))
});
}
Expand All @@ -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();
Expand All @@ -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: {
Expand All @@ -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;
Expand Down
Loading