diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx index 1b3ba6dd..30fd193db 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx @@ -8,6 +8,7 @@ import { GridRow } from 'components/layout/Grid'; import { Item, Tabs } from 'react-basics'; import { useState } from 'react'; import { useMessages } from 'components/hooks'; +import SessionsWeekly from './SessionsWeekly'; export function SessionsPage({ websiteId }) { const [tab, setTab] = useState('activity'); @@ -17,8 +18,9 @@ export function SessionsPage({ websiteId }) { <> - - + + + setTab(value)} style={{ marginBottom: 30 }}> {formatMessage(labels.activity)} diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.module.css b/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.module.css new file mode 100644 index 00000000..4b41d87c --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.module.css @@ -0,0 +1,40 @@ +.week { + display: flex; + justify-content: space-between; + position: relative; +} + +.header { + text-align: center; + font-weight: 700; + margin-bottom: 10px; +} + +.day { + display: flex; + flex-direction: column; + gap: 5px; + position: relative; +} + +.cell { + display: flex; + align-items: center; + justify-content: center; + background-color: var(--base100); + width: 20px; + height: 20px; +} + +.hour { + font-weight: 700; + color: var(--font-color300); + background-color: transparent; +} + +.block { + background-color: var(--primary400); + width: 20px; + height: 20px; + border-radius: 3px; +} diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx new file mode 100644 index 00000000..6c0984ea --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx @@ -0,0 +1,78 @@ +import { format } from 'date-fns'; +import { useLocale, useMessages, useWebsiteSessionsWeekly } from 'components/hooks'; +import { LoadingPanel } from 'components/common/LoadingPanel'; +import { getDayOfWeekAsDate } from 'lib/date'; +import styles from './SessionsWeekly.module.css'; +import classNames from 'classnames'; +import { TooltipPopup } from 'react-basics'; + +export function SessionsWeekly({ websiteId }: { websiteId: string }) { + const { data, ...props } = useWebsiteSessionsWeekly(websiteId); + const { dateLocale } = useLocale(); + const { labels, formatMessage } = useMessages(); + + const [, max] = data + ? data.reduce((arr: number[], hours: number[], index: number) => { + const min = Math.min(...hours); + const max = Math.max(...hours); + + if (index === 0) { + return [min, max]; + } + + if (min < arr[0]) { + arr[0] = min; + } + + if (max > arr[1]) { + arr[1] = max; + } + + return arr; + }, []) + : []; + + return ( + +
+
+
 
+ {Array(24) + .fill(null) + .map((_, i) => { + return ( +
+ {i.toString().padStart(2, '0')} +
+ ); + })} +
+ {data?.map((day: number[], index: number) => { + return ( +
+
+ {format(getDayOfWeekAsDate(index), 'EEE', { locale: dateLocale })} +
+ {day?.map((hour: number) => { + return ( +
+ {hour > 0 && ( + +
+ + )} +
+ ); + })} +
+ ); + })} +
+ + ); +} + +export default SessionsWeekly; diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx index def3f0ba..66393493 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx @@ -12,8 +12,8 @@ export function SessionActivity({ }: { websiteId: string; sessionId: string; - startDate: string; - endDate: string; + startDate: Date; + endDate: Date; }) { const { formatDate } = useTimezone(); const { data, isLoading } = useSessionActivity(websiteId, sessionId, startDate, endDate); diff --git a/src/components/charts/BubbleChart.tsx b/src/components/charts/BubbleChart.tsx new file mode 100644 index 00000000..956e260c --- /dev/null +++ b/src/components/charts/BubbleChart.tsx @@ -0,0 +1,27 @@ +import { Chart, ChartProps } from 'components/charts/Chart'; +import { useState } from 'react'; +import { StatusLight } from 'react-basics'; +import { formatLongNumber } from 'lib/format'; + +export interface BubbleChartProps extends ChartProps { + type?: 'bubble'; +} + +export default function BubbleChart(props: BubbleChartProps) { + const [tooltip, setTooltip] = useState(null); + const { type = 'bubble' } = props; + + const handleTooltip = ({ tooltip }) => { + const { labelColors, dataPoints } = tooltip; + + setTooltip( + tooltip.opacity ? ( + + {formatLongNumber(dataPoints?.[0]?.raw)} {dataPoints?.[0]?.label} + + ) : null, + ); + }; + + return ; +} diff --git a/src/components/common/LoadingPanel.module.css b/src/components/common/LoadingPanel.module.css index 2dc8b75e..00d6cbb4 100644 --- a/src/components/common/LoadingPanel.module.css +++ b/src/components/common/LoadingPanel.module.css @@ -3,4 +3,14 @@ flex-direction: column; position: relative; flex: 1; + height: 100%; +} + +.loading { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; } diff --git a/src/components/common/LoadingPanel.tsx b/src/components/common/LoadingPanel.tsx index 487252be..36de9365 100644 --- a/src/components/common/LoadingPanel.tsx +++ b/src/components/common/LoadingPanel.tsx @@ -1,9 +1,9 @@ import { ReactNode } from 'react'; -import styles from './LoadingPanel.module.css'; import classNames from 'classnames'; -import ErrorMessage from 'components/common/ErrorMessage'; import { Loading } from 'react-basics'; +import ErrorMessage from 'components/common/ErrorMessage'; import Empty from 'components/common/Empty'; +import styles from './LoadingPanel.module.css'; export function LoadingPanel({ data, @@ -27,7 +27,7 @@ export function LoadingPanel({ return (
- {isLoading && !isFetched && } + {isLoading && !isFetched && } {error && } {!error && isEmpty && } {!error && !isEmpty && data && children} diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts index 4e9c49d6..1be99732 100644 --- a/src/components/hooks/index.ts +++ b/src/components/hooks/index.ts @@ -14,6 +14,7 @@ export * from './queries/useSessionDataProperties'; export * from './queries/useSessionDataValues'; export * from './queries/useWebsiteSession'; export * from './queries/useWebsiteSessions'; +export * from './queries/useWebsiteSessionsWeekly'; export * from './queries/useShareToken'; export * from './queries/useTeam'; export * from './queries/useTeams'; diff --git a/src/components/hooks/queries/useSessionActivity.ts b/src/components/hooks/queries/useSessionActivity.ts index 94676a99..16c139ab 100644 --- a/src/components/hooks/queries/useSessionActivity.ts +++ b/src/components/hooks/queries/useSessionActivity.ts @@ -3,15 +3,19 @@ import { useApi } from './useApi'; export function useSessionActivity( websiteId: string, sessionId: string, - startDate: string, - endDate: string, + startDate: Date, + endDate: Date, ) { const { get, useQuery } = useApi(); return useQuery({ - queryKey: ['session:activity', { websiteId, sessionId }], + queryKey: ['session:activity', { websiteId, sessionId, startDate, endDate }], queryFn: () => { - return get(`/websites/${websiteId}/sessions/${sessionId}/activity`, { startDate, endDate }); + return get(`/websites/${websiteId}/sessions/${sessionId}/activity`, { + startAt: +new Date(startDate), + endAt: +new Date(endDate), + }); }, + enabled: Boolean(websiteId && sessionId && startDate && endDate), }); } diff --git a/src/components/hooks/queries/useWebsiteSessionsWeekly.ts b/src/components/hooks/queries/useWebsiteSessionsWeekly.ts new file mode 100644 index 00000000..5df543f5 --- /dev/null +++ b/src/components/hooks/queries/useWebsiteSessionsWeekly.ts @@ -0,0 +1,24 @@ +import { useApi } from './useApi'; +import useModified from '../useModified'; +import { useFilterParams } from 'components/hooks/useFilterParams'; + +export function useWebsiteSessionsWeekly( + websiteId: string, + params?: { [key: string]: string | number }, +) { + const { get, useQuery } = useApi(); + const { modified } = useModified(`sessions`); + const filters = useFilterParams(websiteId); + + return useQuery({ + queryKey: ['sessions', { websiteId, modified, ...params, ...filters }], + queryFn: () => { + return get(`/websites/${websiteId}/sessions/weekly`, { + ...params, + ...filters, + }); + }, + }); +} + +export default useWebsiteSessionsWeekly; diff --git a/src/components/hooks/useFilterParams.ts b/src/components/hooks/useFilterParams.ts index 343aea9f..525f3492 100644 --- a/src/components/hooks/useFilterParams.ts +++ b/src/components/hooks/useFilterParams.ts @@ -1,19 +1,18 @@ import { useNavigation } from './useNavigation'; import { useDateRange } from './useDateRange'; import { useTimezone } from './useTimezone'; -import { zonedTimeToUtc } from 'date-fns-tz'; export function useFilterParams(websiteId: string) { const { dateRange } = useDateRange(websiteId); const { startDate, endDate, unit } = dateRange; - const { timezone } = useTimezone(); + const { timezone, toUtc } = useTimezone(); const { query: { url, referrer, title, query, host, os, browser, device, country, region, city, event }, } = useNavigation(); return { - startAt: +zonedTimeToUtc(startDate, timezone), - endAt: +zonedTimeToUtc(endDate, timezone), + startAt: +toUtc(startDate), + endAt: +toUtc(endDate), unit, timezone, url, diff --git a/src/components/hooks/useTimezone.ts b/src/components/hooks/useTimezone.ts index b5e58ea9..24cef02c 100644 --- a/src/components/hooks/useTimezone.ts +++ b/src/components/hooks/useTimezone.ts @@ -1,6 +1,6 @@ import { setItem } from 'next-basics'; import { TIMEZONE_CONFIG } from 'lib/constants'; -import { formatInTimeZone } from 'date-fns-tz'; +import { formatInTimeZone, zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz'; import useStore, { setTimezone } from 'store/app'; const selector = (state: { timezone: string }) => state.timezone; @@ -23,7 +23,15 @@ export function useTimezone() { ); }; - return { timezone, saveTimezone, formatDate }; + const toUtc = (date: Date | string | number) => { + return zonedTimeToUtc(date, timezone); + }; + + const fromUtc = (date: Date | string | number) => { + return utcToZonedTime(date, timezone); + }; + + return { timezone, saveTimezone, formatDate, toUtc, fromUtc }; } export default useTimezone; diff --git a/src/declaration.d.ts b/src/declaration.d.ts index d968c14d..986adf27 100644 --- a/src/declaration.d.ts +++ b/src/declaration.d.ts @@ -1,5 +1,4 @@ declare module 'cors'; -declare module 'dateformat'; declare module 'debug'; declare module 'chartjs-adapter-date-fns'; declare module 'md5'; diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts index 78f0323e..474417b9 100644 --- a/src/lib/clickhouse.ts +++ b/src/lib/clickhouse.ts @@ -1,4 +1,5 @@ import { ClickHouseClient, createClient } from '@clickhouse/client'; +import { formatInTimeZone } from 'date-fns-tz'; import debug from 'debug'; import { CLICKHOUSE } from 'lib/db'; import { DEFAULT_PAGE_SIZE, OPERATORS } from './constants'; @@ -8,6 +9,7 @@ import { filtersToArray } from './params'; import { PageParams, QueryFilters, QueryOptions } from './types'; export const CLICKHOUSE_DATE_FORMATS = { + utc: '%Y-%m-%dT%H:%i:%SZ', second: '%Y-%m-%d %H:%i:%S', minute: '%Y-%m-%d %H:%i:00', hour: '%Y-%m-%d %H:00:00', @@ -47,7 +49,11 @@ function getClient() { return client; } -function getDateStringSQL(data: any, unit: string | number, timezone?: string) { +function getUTCString(date?: Date) { + return formatInTimeZone(date || new Date(), 'UTC', 'yyyy-MM-dd HH:mm:ss'); +} + +function getDateStringSQL(data: any, unit: string = 'utc', timezone?: string) { if (timezone) { return `formatDateTime(${data}, '${CLICKHOUSE_DATE_FORMATS[unit]}', '${timezone}')`; } @@ -220,6 +226,7 @@ export default { getDateStringSQL, getDateSQL, getFilterQuery, + getUTCString, parseFilters, pagedQuery, findUnique, diff --git a/src/lib/date.ts b/src/lib/date.ts index 2fb24073..b8bfa6c7 100644 --- a/src/lib/date.ts +++ b/src/lib/date.ts @@ -34,6 +34,7 @@ import { addWeeks, subWeeks, endOfMinute, + isSameDay, } from 'date-fns'; import { getDateLocale } from 'lib/lang'; import { DateRange } from 'lib/types'; @@ -336,3 +337,16 @@ export function getCompareDate(compare: string, startDate: Date, endDate: Date) return { startDate: subMinutes(startDate, diff), endDate: subMinutes(endDate, diff) }; } + +export function getDayOfWeekAsDate(dayOfWeek: number) { + const startOfWeekDay = startOfWeek(new Date()); + const daysToAdd = [0, 1, 2, 3, 4, 5, 6].indexOf(dayOfWeek); + let currentDate = addDays(startOfWeekDay, daysToAdd); + + // Ensure we're not returning a past date + if (isSameDay(currentDate, startOfWeekDay)) { + currentDate = addDays(currentDate, 7); + } + + return currentDate; +} diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 28835414..1fbb3cd4 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -81,6 +81,18 @@ function getDateSQL(field: string, unit: string, timezone?: string): string { } } +function getDateWeeklySQL(field: string) { + const db = getDatabaseType(); + + if (db === POSTGRESQL) { + return `EXTRACT(DOW FROM ${field})`; + } + + if (db === MYSQL) { + return `DAYOFWEEK(${field})-1`; + } +} + export function getTimestampSQL(field: string) { const db = getDatabaseType(); @@ -284,6 +296,7 @@ export default { getCastColumnQuery, getDayDiffQuery, getDateSQL, + getDateWeeklySQL, getFilterQuery, getSearchParameters, getTimestampDiffSQL, diff --git a/src/pages/api/websites/[websiteId]/session-data/properties.ts b/src/pages/api/websites/[websiteId]/session-data/properties.ts index 4cd2e1e6..92e182d2 100644 --- a/src/pages/api/websites/[websiteId]/session-data/properties.ts +++ b/src/pages/api/websites/[websiteId]/session-data/properties.ts @@ -6,7 +6,7 @@ import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getSessionDataProperties } from 'queries'; import * as yup from 'yup'; -export interface EventDataFieldsRequestQuery { +export interface SessionDataFieldsRequestQuery { websiteId: string; startAt: string; endAt: string; @@ -23,7 +23,7 @@ const schema = { }; export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); diff --git a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/activity.ts b/src/pages/api/websites/[websiteId]/sessions/[sessionId]/activity.ts index d1a763fb..2b0fc084 100644 --- a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/activity.ts +++ b/src/pages/api/websites/[websiteId]/sessions/[sessionId]/activity.ts @@ -9,16 +9,16 @@ import { getSessionActivity } from 'queries'; export interface SessionActivityRequestQuery extends PageParams { websiteId: string; sessionId: string; - startDate: string; - endDate: string; + startAt: number; + endAt: number; } const schema = { GET: yup.object().shape({ websiteId: yup.string().uuid().required(), sessionId: yup.string().uuid().required(), - startDate: yup.string().required(), - endDate: yup.string().required(), + startAt: yup.number().integer(), + endAt: yup.number().integer(), }), }; @@ -30,19 +30,17 @@ export default async ( await useAuth(req, res); await useValidate(schema, req, res); - const { websiteId, sessionId, startDate, endDate } = req.query; + const { websiteId, sessionId, startAt, endAt } = req.query; if (req.method === 'GET') { if (!(await canViewWebsite(req.auth, websiteId))) { return unauthorized(res); } - const data = await getSessionActivity( - websiteId, - sessionId, - new Date(startDate + 'Z'), - new Date(endDate + 'Z'), - ); + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getSessionActivity(websiteId, sessionId, startDate, endDate); return ok(res, data); } diff --git a/src/pages/api/websites/[websiteId]/sessions/weekly.ts b/src/pages/api/websites/[websiteId]/sessions/weekly.ts new file mode 100644 index 00000000..f33970d0 --- /dev/null +++ b/src/pages/api/websites/[websiteId]/sessions/weekly.ts @@ -0,0 +1,47 @@ +import * as yup from 'yup'; +import { canViewWebsite } from 'lib/auth'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; +import { NextApiRequestQueryBody, PageParams } from 'lib/types'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { pageInfo } from 'lib/schema'; +import { getWebsiteSessionsWeekly } from 'queries'; + +export interface ReportsRequestQuery extends PageParams { + websiteId: string; +} + +const schema = { + GET: yup.object().shape({ + websiteId: yup.string().uuid().required(), + startAt: yup.number().integer().required(), + endAt: yup.number().integer().min(yup.ref('startAt')).required(), + ...pageInfo, + }), +}; + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useCors(req, res); + await useAuth(req, res); + await useValidate(schema, req, res); + + const { websiteId, startAt, endAt } = req.query; + + if (req.method === 'GET') { + if (!(await canViewWebsite(req.auth, websiteId))) { + return unauthorized(res); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getWebsiteSessionsWeekly(websiteId, { startDate, endDate }); + + return ok(res, data); + } + + return methodNotAllowed(res); +}; diff --git a/src/queries/analytics/events/saveEvent.ts b/src/queries/analytics/events/saveEvent.ts index 5e21e303..6c0f917b 100644 --- a/src/queries/analytics/events/saveEvent.ts +++ b/src/queries/analytics/events/saveEvent.ts @@ -135,10 +135,10 @@ async function clickhouseQuery(data: { city, ...args } = data; - const { insert } = clickhouse; + const { insert, getUTCString } = clickhouse; const { sendMessage } = kafka; const eventId = uuid(); - const createdAt = new Date().toISOString(); + const createdAt = getUTCString(); const message = { ...args, diff --git a/src/queries/analytics/sessions/getSessionActivity.ts b/src/queries/analytics/sessions/getSessionActivity.ts index bb7c141c..c50a82d9 100644 --- a/src/queries/analytics/sessions/getSessionActivity.ts +++ b/src/queries/analytics/sessions/getSessionActivity.ts @@ -23,6 +23,7 @@ async function relationalQuery( websiteId, createdAt: { gte: startDate, lte: endDate }, }, + take: 500, }); } @@ -37,8 +38,6 @@ async function clickhouseQuery( return rawQuery( ` select - session_id as id, - website_id as websiteId, created_at as createdAt, url_path as urlPath, url_query as urlQuery, @@ -52,6 +51,7 @@ async function clickhouseQuery( and session_id = {sessionId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} order by created_at desc + limit 500 `, { websiteId, sessionId, startDate, endDate }, ); diff --git a/src/queries/analytics/sessions/getWebsiteSession.ts b/src/queries/analytics/sessions/getWebsiteSession.ts index 22c18642..6f672e7d 100644 --- a/src/queries/analytics/sessions/getWebsiteSession.ts +++ b/src/queries/analytics/sessions/getWebsiteSession.ts @@ -19,7 +19,7 @@ async function relationalQuery(websiteId: string, sessionId: string) { } async function clickhouseQuery(websiteId: string, sessionId: string) { - const { rawQuery } = clickhouse; + const { rawQuery, getDateStringSQL } = clickhouse; return rawQuery( ` @@ -34,8 +34,8 @@ async function clickhouseQuery(websiteId: string, sessionId: string) { country, subdivision1, city, - min(min_time) as firstAt, - max(max_time) as lastAt, + ${getDateStringSQL('min(min_time)')} as firstAt, + ${getDateStringSQL('max(max_time)')} as lastAt, uniq(visit_id) visits, sum(views) as views, sum(events) as events, diff --git a/src/queries/analytics/sessions/getWebsiteSessions.ts b/src/queries/analytics/sessions/getWebsiteSessions.ts index 60f30b6b..1ea3ef49 100644 --- a/src/queries/analytics/sessions/getWebsiteSessions.ts +++ b/src/queries/analytics/sessions/getWebsiteSessions.ts @@ -24,7 +24,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar } async function clickhouseQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) { - const { pagedQuery, parseFilters } = clickhouse; + const { pagedQuery, parseFilters, getDateStringSQL } = clickhouse; const { params, dateQuery, filterQuery } = await parseFilters(websiteId, filters); return pagedQuery( @@ -42,8 +42,8 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar country, subdivision1, city, - min(min_time) as firstAt, - max(max_time) as lastAt, + ${getDateStringSQL('min(min_time)')} as firstAt, + ${getDateStringSQL('max(max_time)')} as lastAt, uniq(visit_id) as visits, sumIf(views, event_type = 1) as views from website_event_stats_hourly diff --git a/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts b/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts new file mode 100644 index 00000000..9031edf5 --- /dev/null +++ b/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts @@ -0,0 +1,69 @@ +import prisma from 'lib/prisma'; +import clickhouse from 'lib/clickhouse'; +import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db'; +import { QueryFilters } from 'lib/types'; + +export async function getWebsiteSessionsWeekly( + ...args: [websiteId: string, filters?: QueryFilters] +) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery(websiteId: string, filters: QueryFilters) { + const { rawQuery, getDateWeeklySQL, parseFilters } = prisma; + const { params } = await parseFilters(websiteId, filters); + + return rawQuery( + ` + select + ${getDateWeeklySQL('created_at')} as time, + count(distinct session_id) as value + from website_event_stats_hourly + where website_id = {{websiteId::uuid}} + and created_at between {{startDate}} and {{endDate}} + group by time + order by 2 + `, + params, + ).then(formatResults); +} + +async function clickhouseQuery(websiteId: string, filters: QueryFilters) { + const { rawQuery } = clickhouse; + const { startDate, endDate } = filters; + + return rawQuery( + ` + select + formatDateTime(created_at, '%w:%H') as time, + count(distinct session_id) as value + from website_event_stats_hourly + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + group by time + order by time + `, + { websiteId, startDate, endDate }, + ).then(formatResults); +} + +function formatResults(data: any) { + const days = []; + + for (let i = 0; i < 7; i++) { + days.push([]); + + for (let j = 0; j < 24; j++) { + days[i].push( + Number( + data.find(({ time }) => time === `${i}:${j.toString().padStart(2, '0')}`)?.value || 0, + ), + ); + } + } + + return days; +} diff --git a/src/queries/analytics/sessions/saveSessionData.ts b/src/queries/analytics/sessions/saveSessionData.ts index d932f7ed..5259239a 100644 --- a/src/queries/analytics/sessions/saveSessionData.ts +++ b/src/queries/analytics/sessions/saveSessionData.ts @@ -80,9 +80,9 @@ async function clickhouseQuery(data: { }) { const { websiteId, sessionId, sessionData } = data; - const { insert } = clickhouse; + const { insert, getUTCString } = clickhouse; const { sendMessages } = kafka; - const createdAt = new Date().toISOString(); + const createdAt = getUTCString(); const jsonKeys = flattenJSON(sessionData); diff --git a/src/queries/index.ts b/src/queries/index.ts index f9c44dba..26c1df09 100644 --- a/src/queries/index.ts +++ b/src/queries/index.ts @@ -26,6 +26,7 @@ export * from './analytics/sessions/getSessionDataProperties'; export * from './analytics/sessions/getSessionDataValues'; export * from './analytics/sessions/getSessionMetrics'; export * from './analytics/sessions/getWebsiteSessions'; +export * from './analytics/sessions/getWebsiteSessionsWeekly'; export * from './analytics/sessions/getSessionActivity'; export * from './analytics/sessions/getSessionStats'; export * from './analytics/sessions/saveSessionData';