From a15d0ca94a545420560c902969aac7eb8fa9b0f1 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 23 Aug 2024 19:23:04 -0700 Subject: [PATCH] fix UTC issues --- src/components/hooks/queries/useRealtime.ts | 8 +++++--- src/lib/prisma.ts | 17 +++++++++-------- src/pages/api/realtime/[websiteId].ts | 7 +++++-- .../api/websites/[websiteId]/sessions/weekly.ts | 7 +++++-- src/queries/analytics/getRealtimeData.ts | 11 +++++++---- .../sessions/getWebsiteSessionsWeekly.ts | 6 ++++-- 6 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/components/hooks/queries/useRealtime.ts b/src/components/hooks/queries/useRealtime.ts index 88bf06e4b1..9c665e4f06 100644 --- a/src/components/hooks/queries/useRealtime.ts +++ b/src/components/hooks/queries/useRealtime.ts @@ -1,13 +1,15 @@ +import { useTimezone } from 'components/hooks'; +import { REALTIME_INTERVAL } from 'lib/constants'; import { RealtimeData } from 'lib/types'; import { useApi } from './useApi'; -import { REALTIME_INTERVAL } from 'lib/constants'; export function useRealtime(websiteId: string) { const { get, useQuery } = useApi(); + const { timezone } = useTimezone(); const { data, isLoading, error } = useQuery({ - queryKey: ['realtime', websiteId], + queryKey: ['realtime', { websiteId, timezone }], queryFn: async () => { - return get(`/realtime/${websiteId}`); + return get(`/realtime/${websiteId}`, { timezone }); }, enabled: !!websiteId, refetchInterval: REALTIME_INTERVAL, diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 844238b9a1..f0f071bc79 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -12,11 +12,11 @@ import { filtersToArray } from './params'; const log = debug('umami:prisma'); const MYSQL_DATE_FORMATS = { - minute: '%Y-%m-%dT%H:%i:00Z', - hour: '%Y-%m-%dT%H:00:00Z', - day: '%Y-%m-%dT00:00:00Z', - month: '%Y-%m-01T00:00:00Z', - year: '%Y-01-01T00:00:00Z', + minute: '%Y-%m-%dT%H:%i:00', + hour: '%Y-%m-%d %H:00:00', + day: '%Y-%m-%d', + month: '%Y-%m-01', + year: '%Y-01-01', }; const POSTGRESQL_DATE_FORMATS = { @@ -82,15 +82,16 @@ function getDateSQL(field: string, unit: string, timezone?: string): string { } } -function getDateWeeklySQL(field: string) { +function getDateWeeklySQL(field: string, timezone?: string) { const db = getDatabaseType(); if (db === POSTGRESQL) { - return `concat(extract(dow from ${field}), ':', to_char(${field}, 'HH24'))`; + return `concat(extract(dow from (${field} at time zone '${timezone}')), ':', to_char((${field} at time zone '${timezone}'), 'HH24'))`; } if (db === MYSQL) { - return `date_format(${field}, '%w:%H')`; + const tz = moment.tz(timezone).format('Z'); + return `date_format(convert_tz(${field},'+00:00','${tz}'), '%w:%H')`; } } diff --git a/src/pages/api/realtime/[websiteId].ts b/src/pages/api/realtime/[websiteId].ts index 66dcabc97e..08e9bc471e 100644 --- a/src/pages/api/realtime/[websiteId].ts +++ b/src/pages/api/realtime/[websiteId].ts @@ -7,14 +7,17 @@ import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getRealtimeData } from 'queries'; import * as yup from 'yup'; import { REALTIME_RANGE } from 'lib/constants'; +import { TimezoneTest } from 'lib/yup'; export interface RealtimeRequestQuery { websiteId: string; + timezone?: string; } const schema = { GET: yup.object().shape({ websiteId: yup.string().uuid().required(), + timezone: TimezoneTest, }), }; @@ -23,7 +26,7 @@ export default async (req: NextApiRequestQueryBody, res: N await useValidate(schema, req, res); if (req.method === 'GET') { - const { websiteId } = req.query; + const { websiteId, timezone } = req.query; if (!(await canViewWebsite(req.auth, websiteId))) { return unauthorized(res); @@ -31,7 +34,7 @@ export default async (req: NextApiRequestQueryBody, res: N const startDate = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); - const data = await getRealtimeData(websiteId, { startDate }); + const data = await getRealtimeData(websiteId, { startDate, timezone }); return ok(res, data); } diff --git a/src/pages/api/websites/[websiteId]/sessions/weekly.ts b/src/pages/api/websites/[websiteId]/sessions/weekly.ts index f33970d0b7..b1c28c3f6f 100644 --- a/src/pages/api/websites/[websiteId]/sessions/weekly.ts +++ b/src/pages/api/websites/[websiteId]/sessions/weekly.ts @@ -6,9 +6,11 @@ import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { pageInfo } from 'lib/schema'; import { getWebsiteSessionsWeekly } from 'queries'; +import { TimezoneTest } from 'lib/yup'; export interface ReportsRequestQuery extends PageParams { websiteId: string; + timezone?: string; } const schema = { @@ -16,6 +18,7 @@ const schema = { websiteId: yup.string().uuid().required(), startAt: yup.number().integer().required(), endAt: yup.number().integer().min(yup.ref('startAt')).required(), + timezone: TimezoneTest, ...pageInfo, }), }; @@ -28,7 +31,7 @@ export default async ( await useAuth(req, res); await useValidate(schema, req, res); - const { websiteId, startAt, endAt } = req.query; + const { websiteId, startAt, endAt, timezone } = req.query; if (req.method === 'GET') { if (!(await canViewWebsite(req.auth, websiteId))) { @@ -38,7 +41,7 @@ export default async ( const startDate = new Date(+startAt); const endDate = new Date(+endAt); - const data = await getWebsiteSessionsWeekly(websiteId, { startDate, endDate }); + const data = await getWebsiteSessionsWeekly(websiteId, { startDate, endDate, timezone }); return ok(res, data); } diff --git a/src/queries/analytics/getRealtimeData.ts b/src/queries/analytics/getRealtimeData.ts index 63aa9aecb8..1af63219d6 100644 --- a/src/queries/analytics/getRealtimeData.ts +++ b/src/queries/analytics/getRealtimeData.ts @@ -1,4 +1,4 @@ -import { getRealtimeActivity, getPageviewStats, getSessionStats } from 'queries/index'; +import { getPageviewStats, getRealtimeActivity, getSessionStats } from 'queries/index'; function increment(data: object, key: string) { if (key) { @@ -10,9 +10,12 @@ function increment(data: object, key: string) { } } -export async function getRealtimeData(websiteId: string, criteria: { startDate: Date }) { - const { startDate } = criteria; - const filters = { startDate, endDate: new Date(), unit: 'minute' }; +export async function getRealtimeData( + websiteId: string, + criteria: { startDate: Date; timezone: string }, +) { + const { startDate, timezone } = criteria; + const filters = { startDate, endDate: new Date(), unit: 'minute', timezone }; const [activity, pageviews, sessions] = await Promise.all([ getRealtimeActivity(websiteId, filters), getPageviewStats(websiteId, filters), diff --git a/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts b/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts index c92c2929be..153c9bb385 100644 --- a/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts +++ b/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts @@ -13,13 +13,14 @@ export async function getWebsiteSessionsWeekly( } async function relationalQuery(websiteId: string, filters: QueryFilters) { + const { timezone = 'utc' } = filters; const { rawQuery, getDateWeeklySQL, parseFilters } = prisma; const { params } = await parseFilters(websiteId, filters); return rawQuery( ` select - ${getDateWeeklySQL('created_at')} as time, + ${getDateWeeklySQL('created_at', timezone)} as time, count(distinct session_id) as value from website_event where website_id = {{websiteId::uuid}} @@ -32,13 +33,14 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { } async function clickhouseQuery(websiteId: string, filters: QueryFilters) { + const { timezone = 'utc' } = filters; const { rawQuery } = clickhouse; const { startDate, endDate } = filters; return rawQuery( ` select - formatDateTime(created_at, '%w:%H') as time, + formatDateTime(toDateTime(created_at, '${timezone}'), '%w:%H') as time, count(distinct session_id) as value from website_event_stats_hourly where website_id = {websiteId:UUID}