diff --git a/src/client/api/model/website.ts b/src/client/api/model/website.ts index 072fdd0..2aa2bb5 100644 --- a/src/client/api/model/website.ts +++ b/src/client/api/model/website.ts @@ -140,5 +140,9 @@ export function useWorkspaceWebsitePageview( } ); - return { stats: data?.stats ?? [], isLoading }; + return { + pageviews: data?.pageviews ?? [], + sessions: data?.sessions ?? [], + isLoading, + }; } diff --git a/src/client/components/WebsiteOverview.tsx b/src/client/components/WebsiteOverview.tsx index 02e5e54..8f0a87a 100644 --- a/src/client/components/WebsiteOverview.tsx +++ b/src/client/components/WebsiteOverview.tsx @@ -47,7 +47,7 @@ const WebsiteOverviewItem: React.FC<{ const startDate = dayjs().subtract(1, 'day').add(1, unit).startOf(unit); const endDate = dayjs().endOf(unit); - const { stats, isLoading } = useWorkspaceWebsitePageview( + const { pageviews, sessions, isLoading } = useWorkspaceWebsitePageview( props.website.workspaceId, props.website.id, startDate.unix() * 1000, @@ -55,8 +55,14 @@ const WebsiteOverviewItem: React.FC<{ ); const chartData = useMemo(() => { - return getDateArray(stats, startDate, endDate, unit); - }, [stats, unit]); + const pageviewsArr = getDateArray(pageviews, startDate, endDate, unit); + const sessionsArr = getDateArray(sessions, startDate, endDate, unit); + + return [ + ...pageviewsArr.map((item) => ({ ...item, type: 'pageview' })), + ...sessionsArr.map((item) => ({ ...item, type: 'session' })), + ]; + }, [pageviews, sessions, unit]); if (isLoading) { return ; @@ -138,14 +144,16 @@ const MetricCard: React.FC<{ MetricCard.displayName = 'MetricCard'; export const StatsChart: React.FC<{ - data: { x: string; y: number }[]; + data: { x: string; y: number; type: string }[]; unit: DateUnit; }> = React.memo((props) => { const config: ColumnConfig = useMemo( () => ({ data: props.data, + isStack: true, xField: 'x', yField: 'y', + seriesField: 'type', label: { position: 'middle' as const, style: { diff --git a/src/server/model/workspace.ts b/src/server/model/workspace.ts index 47e1156..c21c73b 100644 --- a/src/server/model/workspace.ts +++ b/src/server/model/workspace.ts @@ -104,32 +104,6 @@ export async function deleteWorkspaceWebsite( return website; } -export async function getWorkspaceWebsitePageviewStats( - websiteId: string, - filters: QueryFilters -) { - const { timezone = 'utc', unit = 'day' } = filters; - const { filterQuery, joinSession, params } = await parseFilters(websiteId, { - ...filters, - eventType: EVENT_TYPE.pageView, - }); - - return prisma.$queryRaw` - select - ${getDateQuery('"WebsiteEvent"."createdAt"', unit, timezone)} x, - count(1) y - from "WebsiteEvent" - ${joinSession ? Prisma.sql([joinSession]) : Prisma.empty} - where "WebsiteEvent"."websiteId" = ${params.websiteId}::uuid - and "WebsiteEvent"."createdAt" between ${ - params.startDate - }::timestamptz and ${params.endDate}::timestamptz - and "WebsiteEvent"."eventType" = ${EVENT_TYPE.pageView} - ${filterQuery} - group by 1 - `; -} - export async function getWorkspaceWebsiteDateRange(websiteId: string) { const { params } = await parseFilters(websiteId, { startDate: new Date(DEFAULT_RESET_DATE), @@ -155,3 +129,53 @@ export async function getWorkspaceWebsiteDateRange(websiteId: string) { min: res._min.createdAt, }; } + +export async function getWorkspaceWebsitePageviewStats( + websiteId: string, + filters: QueryFilters +) { + const { timezone = 'utc', unit = 'day' } = filters; + const { filterQuery, joinSession, params } = await parseFilters(websiteId, { + ...filters, + }); + + return prisma.$queryRaw` + select + ${getDateQuery('"WebsiteEvent"."createdAt"', unit, timezone)} x, + count(1) y + from "WebsiteEvent" + ${joinSession} + where "WebsiteEvent"."websiteId" = ${params.websiteId}::uuid + and "WebsiteEvent"."createdAt" between ${ + params.startDate + }::timestamptz and ${params.endDate}::timestamptz + and "WebsiteEvent"."eventType" = ${EVENT_TYPE.pageView} + ${filterQuery} + group by 1 + `; +} + +export async function getWorkspaceWebsiteSessionStats( + websiteId: string, + filters: QueryFilters +) { + const { timezone = 'utc', unit = 'day' } = filters; + const { filterQuery, joinSession, params } = await parseFilters(websiteId, { + ...filters, + }); + + return prisma.$queryRaw` + select + ${getDateQuery('"WebsiteEvent"."createdAt"', unit, timezone)} x, + count(distinct "WebsiteEvent"."sessionId") y + from "WebsiteEvent" + ${joinSession} + where "WebsiteEvent"."websiteId" = ${params.websiteId}::uuid + and "WebsiteEvent"."createdAt" between ${ + params.startDate + }::timestamptz and ${params.endDate}::timestamptz + and "WebsiteEvent"."eventType" = ${EVENT_TYPE.pageView} + ${filterQuery} + group by 1 + `; +} diff --git a/src/server/router/workspace.ts b/src/server/router/workspace.ts index 53c6ba6..eea6042 100644 --- a/src/server/router/workspace.ts +++ b/src/server/router/workspace.ts @@ -8,6 +8,7 @@ import { getWorkspaceWebsiteInfo, getWorkspaceWebsitePageviewStats, getWorkspaceWebsites, + getWorkspaceWebsiteSessionStats, updateWorkspaceWebsiteInfo, } from '../model/workspace'; import { parseDateRange } from '../utils/common'; @@ -186,11 +187,11 @@ workspaceRouter.get( city, }; - const stats = await getWorkspaceWebsitePageviewStats( - websiteId, - filters as QueryFilters - ); + const [pageviews, sessions] = await Promise.all([ + getWorkspaceWebsitePageviewStats(websiteId, filters as QueryFilters), + getWorkspaceWebsiteSessionStats(websiteId, filters as QueryFilters), + ]); - res.json({ stats }); + res.json({ pageviews, sessions }); } ); diff --git a/src/server/utils/prisma.ts b/src/server/utils/prisma.ts index c95605e..c9b92c2 100644 --- a/src/server/utils/prisma.ts +++ b/src/server/utils/prisma.ts @@ -58,8 +58,10 @@ export async function parseFilters( ([key, value]) => typeof value !== 'undefined' && SESSION_COLUMNS.includes(key) ) - ? `inner join "WebsiteSession" on "WebsiteEvent"."sessionId" = "WebsiteSession"."id"` - : '', + ? Prisma.sql([ + `inner join "WebsiteSession" on "WebsiteEvent"."sessionId" = "WebsiteSession"."id"`, + ]) + : Prisma.empty, filterQuery: getFilterQuery(filters, options, websiteDomain), params: { ...normalizeFilters(filters),